AutomatosX commited on
Commit
6708e4d
·
verified ·
1 Parent(s): fd0c04f

Publish verified OCR-aware MXFP8 checkpoint

Browse files
Files changed (50) hide show
  1. README.md +81 -27
  2. benchmarks/datasets.md +36 -1
  3. benchmarks/evaluate_tables.py +1 -2
  4. benchmarks/rswa_validation.py +83 -23
  5. benchmarks/run_accuracy.py +241 -33
  6. benchmarks/run_performance.py +55 -5
  7. config.json +0 -10
  8. docs/PRD.md +2 -0
  9. docs/TECHNICAL_SPEC.md +70 -4
  10. docs/adr/0002-calibrated-affine8-lm-head.md +9 -3
  11. examples/single_image.py +1 -1
  12. model.safetensors +2 -2
  13. model.safetensors.index.json +1 -2
  14. precision_map.json +5 -5
  15. preprocessing_config.json +24 -1
  16. pyproject.toml +16 -7
  17. quantization/calibrate_precision.py +169 -41
  18. quantization/layer_sensitivity.py +21 -9
  19. quantization/mixed_precision_convert.py +40 -3
  20. quantization/release_gate.py +1714 -160
  21. quantization/run_pipeline.py +299 -26
  22. quantization_config.json +9 -8
  23. quantization_summary.json +8 -9
  24. release/bf16_accuracy.json +128 -72
  25. release/calibration_affine8_accuracy.json +221 -0
  26. release/calibration_affine8_performance.json +69 -0
  27. release/calibration_baseline_accuracy.json +221 -0
  28. release/calibration_bfloat16_accuracy.json +221 -0
  29. release/calibration_bfloat16_performance.json +69 -0
  30. release/calibration_mxfp8_accuracy.json +221 -0
  31. release/calibration_mxfp8_performance.json +69 -0
  32. release/calibration_reference_performance.json +69 -0
  33. release/calibration_results.json +187 -26
  34. release/candidate_accuracy.json +133 -77
  35. release/candidate_performance.json +24 -23
  36. release/candidate_rswa.json +21 -16
  37. release/generated_precision_map.json +5 -5
  38. release/provenance.json +162 -43
  39. release/reference_accuracy.json +137 -81
  40. release/reference_performance.json +18 -17
  41. release/release_manifest.json +1163 -263
  42. release/sensitivity_results.json +221 -103
  43. src/unlimited_ocr/__init__.py +5 -2
  44. src/unlimited_ocr/adapter_registry.json +10 -2
  45. src/unlimited_ocr/cli.py +151 -14
  46. src/unlimited_ocr/engine.py +120 -38
  47. src/unlimited_ocr/pdf.py +47 -40
  48. src/unlimited_ocr/pipeline.py +122 -28
  49. src/unlimited_ocr/preprocessing.py +10 -0
  50. tokenizer_config.json +1 -1
README.md CHANGED
@@ -10,9 +10,13 @@ tags:
10
  - mxfp8
11
  - int8
12
  - apple-silicon
 
 
 
 
13
  ---
14
 
15
- # AX Unlimited-OCR 3B MoE — MLX MXFP8
16
 
17
  This is an AutomatosX sensitivity-guided MLX conversion of
18
  [`baidu/Unlimited-OCR`](https://huggingface.co/baidu/Unlimited-OCR). It was
@@ -21,6 +25,14 @@ created from the upstream BF16 weights. The
21
  checkpoint was used only as a comparison reference; these are not repackaged
22
  Sahil weights.
23
 
 
 
 
 
 
 
 
 
24
  ## What was optimized
25
 
26
  Each supported module group was quantized independently and evaluated for OCR
@@ -28,41 +40,74 @@ CER, digit CER, and table structure. Eligible language linear layers use MXFP8
28
  (8 data bits, group size 32). A joint calibration then selected affine INT8
29
  (group size 32) for the LM head because BF16 missed the throughput floor and
30
  MXFP8 missed the CER ceiling. OCR-critical or sensitive groups remain BF16.
31
- Retained groups: vision_encoder.
32
 
33
- The model uses mlx-vlm's native `model_type=unlimited-ocr` implementation and a
34
- sliding-window size of 128.
 
 
35
 
36
  ## AutomatosX validation
37
 
38
- These are local release measurements, not upstream paper benchmark claims.
39
- The content-addressed evaluation set had 12 samples
 
 
 
 
40
  (12 with digits, 3 with
41
  CJK, and 3 with tables), digest
42
- `78ca3e7c9c7584aea30b04176716a6dadf49ccf0f2bead959cbaeb9aeac68f30`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- | Checkpoint | Mean CER | Digit CER | Table score | Decode tok/s |
45
- |---|---:|---:|---:|---:|
46
- | Upstream BF16 | 0.239188 | 0.087340 | 1.000000 | — |
47
- | Sahil MXFP8 reference | 1.057249 | 0.209153 | 1.000000 | 312.73 |
48
- | AutomatosX candidate | 0.237384 | 0.085733 | 1.000000 | 297.20 |
49
 
50
- Weight size: 3.579 GiB
51
- Aggregate weight SHA-256: `a4f6e9934f90564906f42929542ad68493f0c6187ad431128e447debbf52b6f4`
 
52
 
53
  ## Release gates
54
 
55
- - ✅ `weights_are_distinct` — actual `a4f6e9934f90564906f42929542ad68493f0c6187ad431128e447debbf52b6f4`, limit `different from 439c6acfe5e277537dfe3368b94a145a6b0da4c39e4bd00582977d030f292ad1`
56
- - ✅ `weight_size_gb` — actual `3.5792221864685416`, limit `4.5`
57
- - ✅ `native_model_metadata` — actual `{'architecture': True, 'model_type': True, 'mxfp8': True, 'sliding_window': True, 'processor_class': True, 'sft_format': True}`, limit `True`
 
58
  - ✅ `evaluation_coverage` — actual `{'samples': 12, 'digit': 12, 'cjk': 3, 'table': 3}`, limit `all counts > 0`
59
- - ✅ `same_evaluation_samples` — actual `[12, 12, 12]`, limit `12`
60
- - ✅ `candidate_cer_vs_bf16` — actual `-0.0018033315701714847`, limit `0.01`
61
- - ✅ `candidate_cer_vs_reference` — actual `-0.8198643918447561`, limit `0.005`
62
- - ✅ `candidate_digit_cer_vs_bf16` — actual `-0.0016074315849917697`, limit `0.01`
 
 
 
 
 
63
  - ✅ `candidate_table_score_vs_bf16` — actual `0.0`, limit `0.01`
64
- - ✅ `candidate_tps_vs_reference` — actual `0.9503587809120344`, limit `0.9`
65
- - ✅ `rswa_8k_bounded` — actual `{'cache_bounded': True, 'tps_stable': True, '8k_test_passed': True}`, limit `True`
 
 
 
 
 
 
 
66
 
67
  Full per-sample evidence, sensitivity results, precision rules, PRD, ADR, and
68
  technical specification are included under `release/`, `quantization/`, and
@@ -70,8 +115,11 @@ technical specification are included under `release/`, `quantization/`, and
70
 
71
  ## Usage
72
 
 
 
 
73
  ```bash
74
- pip install mlx-vlm
75
  python -m mlx_vlm.generate \
76
  --model AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
77
  --image document.png \
@@ -79,6 +127,12 @@ python -m mlx_vlm.generate \
79
  --max-tokens 4096
80
  ```
81
 
82
- Apple Silicon and a recent `mlx-vlm` release with native Unlimited-OCR support
83
- are required. This is quantization/conversion work, not foundation-model
84
- retraining or domain fine-tuning.
 
 
 
 
 
 
 
10
  - mxfp8
11
  - int8
12
  - apple-silicon
13
+ widget:
14
+ - text: "<image>document parsing."
15
+ - text: "<image>Convert the document to markdown."
16
+ - text: "<image>Free OCR. Extract all text."
17
  ---
18
 
19
+ # AX Unlimited-OCR 3B MoE — MLX (MXFP8 + affine INT8 head)
20
 
21
  This is an AutomatosX sensitivity-guided MLX conversion of
22
  [`baidu/Unlimited-OCR`](https://huggingface.co/baidu/Unlimited-OCR). It was
 
25
  checkpoint was used only as a comparison reference; these are not repackaged
26
  Sahil weights.
27
 
28
+ **Hybrid precision (name is shorthand):** MXFP8 (group size 32) on eligible
29
+ language linears, **affine INT8** on the LM head, **BF16** on the vision
30
+ encoder and other protected groups. The Hub “1B params” style size reflects
31
+ active MoE capacity; the marketing name refers to the upstream 3B MoE family.
32
+
33
+ Immutable upstream source revision: `ee63731b6461c8afcdcc7b15352e7d2ffecc2ead`.
34
+ Reference checkpoint revision: `55b8031a6c867de675279d9604e38cc94b9882a4`.
35
+
36
  ## What was optimized
37
 
38
  Each supported module group was quantized independently and evaluated for OCR
 
40
  (8 data bits, group size 32). A joint calibration then selected affine INT8
41
  (group size 32) for the LM head because BF16 missed the throughput floor and
42
  MXFP8 missed the CER ceiling. OCR-critical or sensitive groups remain BF16.
43
+ Retained groups: vision_encoder, vision_projector.
44
 
45
+ The model uses mlx-vlm's native `model_type=unlimited-ocr` implementation,
46
+ `processor_class=UnlimitedOCRHFProcessor`, and a sliding-window size of
47
+ 128. Prompts should be raw Unlimited-OCR strings such as
48
+ `<image>document parsing.` (not multi-turn chat formatting).
49
 
50
  ## AutomatosX validation
51
 
52
+ These are **local release measurements**, not upstream paper benchmark claims
53
+ and **not** OmniDocBench / olmOCR-Bench scores. Absolute CER on this set is
54
+ not a production accuracy bar; gates check **parity with BF16** and packaging
55
+ integrity.
56
+
57
+ The content-addressed held-out evaluation set had 12 samples
58
  (12 with digits, 3 with
59
  CJK, and 3 with tables), digest
60
+ `7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987`.
61
+
62
+ | Checkpoint | Mean CER | Digit CER | CJK CER | Table score | Decode tok/s |
63
+ |---|---:|---:|---:|---:|---:|
64
+ | Upstream BF16 | 0.205212 | 0.113310 | 0.333333 | 1.000000 | — |
65
+ | Community MXFP8 reference | 0.907381 | 0.406511 | 1.000000 | 1.000000 | 35.07 |
66
+ | AutomatosX candidate | 0.216331 | 0.111939 | 0.341270 | 1.000000 | 35.14 |
67
+
68
+ **Caveats**
69
+ - Small local set; some samples are synthetic. Do not treat mean CER as field accuracy.
70
+ - The community reference may use a different `model_type` / load path; it is a
71
+ throughput and packaging baseline, not an identical-recipe peer.
72
+ - Generation profiles used in AutomatosX tooling live in the `unlimited-ocr`
73
+ package (`profiles.py`); weight-only loads use mlx-vlm defaults unless you
74
+ pass the same decoding settings.
75
+
76
+ ### Weight integrity
77
 
78
+ Weight size: 3.582 GiB
79
+ Aggregate weight SHA-256: `5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549`
 
 
 
80
 
81
+ Per-file weight SHA-256:
82
+
83
+ - `model.safetensors`: `d9fe11fc8d2e333000be6a0e288681231efa69282c844f7775ca40f52db02f0c`
84
 
85
  ## Release gates
86
 
87
+ - ✅ `weights_are_distinct` — actual `5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549`, limit `different from 439c6acfe5e277537dfe3368b94a145a6b0da4c39e4bd00582977d030f292ad1`
88
+ - ✅ `weight_size_gb` — actual `3.5815872186794877`, limit `4.5`
89
+ - ✅ `native_model_metadata` — actual `{'architecture': True, 'model_type': True, 'mxfp8': True, 'sliding_window': True, 'processor_class': True, 'sft_format': True, 'precision_map': True, 'quantization_summary': True, 'protected_modules_preserved': True}`, limit `True`
90
+ - ✅ `candidate_source_provenance` — actual `{'model': 'Unlimited-OCR', 'revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead'}`, limit `{'model': ['baidu/Unlimited-OCR', 'Unlimited-OCR'], 'revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead'}`
91
  - ✅ `evaluation_coverage` — actual `{'samples': 12, 'digit': 12, 'cjk': 3, 'table': 3}`, limit `all counts > 0`
92
+ - ✅ `held_out_evaluation_dataset` — actual `{'passed': True, 'calibration_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'evaluation_digest': '7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987', 'overlapping_image_sha256': [], 'overlapping_ground_truth_sha256': []}`, limit `distinct dataset digests and no shared image or ground-truth hashes`
93
+ - ✅ `accuracy_aggregates_recomputed` — actual `[True, True, True]`, limit `[True, True, True]`
94
+ - ✅ `same_evaluation_samples` — actual `{'counts': [12, 12, 12], 'file_counts': [12, 12, 12]}`, limit `{'count': 12, 'files': ['0001.png', '0002.png', '0003.png', '0004.png', '0005.png', '0006.png', '0007.png', '0008.png', '0009.png', '0010.png', '0011.png', '0012.png']}`
95
+ - ✅ `model_identities` — actual `{'bf16': 'Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'candidate': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'reference_performance': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'candidate_performance': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'rswa': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8'}`, limit `{'bf16': 'Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'candidate': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8'}`
96
+ - ✅ `immutable_model_revisions` — actual `{'bf16_accuracy': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_accuracy': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_accuracy': None, 'reference_performance': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_performance': None}`, limit `{'bf16_accuracy': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_accuracy': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_accuracy': None, 'reference_performance': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_performance': None}`
97
+ - ✅ `same_accuracy_recipe` — actual `{'prompt': ['<image>document parsing.', '<image>document parsing.', '<image>document parsing.'], 'max_tokens': [1024, 1024, 1024], 'profile': ['accurate', 'accurate', 'accurate'], 'generation_settings': [{'temperature': 0.0, 'top_p': 1.0, 'repetition_penalty': 1.0, 'no_repeat_ngram_size': 35, 'ngram_window': 128}, {'temperature': 0.0, 'top_p': 1.0, 'repetition_penalty': 1.0, 'no_repeat_ngram_size': 35, 'ngram_window': 128}, {'temperature': 0.0, 'top_p': 1.0, 'repetition_penalty': 1.0, 'no_repeat_ngram_size': 35, 'ngram_window': 128}]}`, limit `identical official MLX OCR recipe`
98
+ - ✅ `candidate_cer_vs_bf16` — actual `0.011118653228923858`, limit `0.015`
99
+ - ✅ `candidate_cer_vs_reference` — actual `-0.691050362380767`, limit `0.005`
100
+ - ✅ `candidate_digit_cer_vs_bf16` — actual `-0.0013706317359285375`, limit `0.01`
101
  - ✅ `candidate_table_score_vs_bf16` — actual `0.0`, limit `0.01`
102
+ - ✅ `performance_aggregates_recomputed` — actual `{'reference': True, 'candidate': True}`, limit `{'reference': True, 'candidate': True}`
103
+ - ✅ `same_performance_setup` — actual `{'image_path': ['test_invoice.png', 'test_invoice.png'], 'prompt': ['<image>document parsing.', '<image>document parsing.'], 'max_tokens': [256, 256], 'num_warmup': [1, 1], 'num_runs': [3, 3], 'system': [{'platform': 'macOS-26.5.2-arm64-arm-64bit-Mach-O', 'processor': 'arm', 'python_version': '3.14.6', 'machine': 'arm64', 'mlx_version': '0.32.0', 'mlx_vlm_version': '0.6.6', 'chip': 'Apple M3 Max', 'total_memory_gb': 128.0}, {'platform': 'macOS-26.5.2-arm64-arm-64bit-Mach-O', 'processor': 'arm', 'python_version': '3.14.6', 'machine': 'arm64', 'mlx_version': '0.32.0', 'mlx_vlm_version': '0.6.6', 'chip': 'Apple M3 Max', 'total_memory_gb': 128.0}]}`, limit `identical setup with at least three complete runs`
104
+ - ✅ `candidate_tps_vs_reference` — actual `1.001955642681865`, limit `0.9`
105
+ - ✅ `rswa_8k_bounded` — actual `{'pass_conditions': {'cache_bounded': True, 'tps_stable': True, '8k_test_passed': True}, 'tokens': 8192, 'repetition_rate': 0.20496894409937888}`, limit `{'min_tokens': 8192, 'max_repetition_rate': 0.25}`
106
+ - ✅ `provenance_matches_release` — actual `{'source': 'baidu/Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'target': 'AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'source_config': {'path': 'config.json', 'sha256': '27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9'}, 'source_revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_revision': '55b8031a6c867de675279d9604e38cc94b9882a4', 'dataset_digest': '7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987', 'calibration_dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb'}`, limit `{'source': 'baidu/Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'target': 'AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'source_config_sha256': '27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9', 'source_revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_revision': '55b8031a6c867de675279d9604e38cc94b9882a4', 'dataset_digest': '7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987', 'calibration_dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb'}`
107
+ - ✅ `calibration_recomputed` — actual `{'selected': {'label': 'affine8-head', 'precision': 'affine8'}, 'models': ['AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8', 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16', 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8'], 'dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'input_artifacts': {'calibration_baseline_accuracy': {'filename': 'calibration_baseline_accuracy.json', 'size': 5680, 'sha256': 'ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6'}, 'calibration_reference_performance': {'filename': 'calibration_reference_performance.json', 'size': 2271, 'sha256': '98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4'}, 'calibration_bfloat16_accuracy': {'filename': 'calibration_bfloat16_accuracy.json', 'size': 5678, 'sha256': '5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0'}, 'calibration_bfloat16_performance': {'filename': 'calibration_bfloat16_performance.json', 'size': 2244, 'sha256': 'dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0'}, 'calibration_mxfp8_accuracy': {'filename': 'calibration_mxfp8_accuracy.json', 'size': 5689, 'sha256': '77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26'}, 'calibration_mxfp8_performance': {'filename': 'calibration_mxfp8_performance.json', 'size': 2252, 'sha256': 'e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b'}, 'calibration_affine8_accuracy': {'filename': 'calibration_affine8_accuracy.json', 'size': 5657, 'sha256': 'c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19'}, 'calibration_affine8_performance': {'filename': 'calibration_affine8_performance.json', 'size': 2247, 'sha256': 'f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f'}}}`, limit `content-addressed inputs and fastest passing experiment`
108
+ - ✅ `sensitivity_matches_calibration_dataset` — actual `{'dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'baseline_metrics_match': True}`, limit `{'dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'baseline_metrics_match': True}`
109
+ - ✅ `candidate_precision_map_matches_evidence` — actual `91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75`, limit `91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75`
110
+ - ✅ `precision_map_reproducible` — actual `c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a`, limit `c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a`
111
 
112
  Full per-sample evidence, sensitivity results, precision rules, PRD, ADR, and
113
  technical specification are included under `release/`, `quantization/`, and
 
115
 
116
  ## Usage
117
 
118
+ Requires **Apple Silicon** and **mlx-vlm ≥ 0.6.4** (native Unlimited-OCR / R-SWA).
119
+ Tested stack for this release: mlx-vlm `0.6.6`, mlx `0.32.0`.
120
+
121
  ```bash
122
+ pip install 'mlx-vlm>=0.6.4'
123
  python -m mlx_vlm.generate \
124
  --model AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
125
  --image document.png \
 
127
  --max-tokens 4096
128
  ```
129
 
130
+ Or with the AutomatosX toolkit (deskew, PDF, profiles):
131
+
132
+ ```bash
133
+ pip install 'unlimited-ocr[mlx]'
134
+ # then use OCRPipeline / the unlimited-ocr CLI with model_path=AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8
135
+ ```
136
+
137
+ This is quantization/conversion work, not foundation-model retraining or domain
138
+ fine-tuning.
benchmarks/datasets.md CHANGED
@@ -35,9 +35,44 @@ The evaluation set must be separate from any calibration data used for quantizat
35
  benchmarks/eval_data/
36
  ├── images/ # Input document images (PNG/JPG)
37
  ├── ground_truth/ # Reference text files (same stem as image)
38
- └── metadata.json # Dataset composition and source info
39
  ```
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  ### Ground Truth Format
42
 
43
  Each ground truth file is a plain `.txt` file containing the expected OCR output.
 
35
  benchmarks/eval_data/
36
  ├── images/ # Input document images (PNG/JPG)
37
  ├── ground_truth/ # Reference text files (same stem as image)
38
+ └── manifest.json # Exact sample paths, categories, and source metadata
39
  ```
40
 
41
+ ### Calibration vs held-out evaluation (required for schema-3 release)
42
+
43
+ Sensitivity / LM-head calibration and the final release accuracy run must use
44
+ **disjoint** directories. Recommended layout under `eval_dataset/`:
45
+
46
+ ```
47
+ eval_dataset/
48
+ ├── calibration/
49
+ │ ├── images/
50
+ │ ├── ground_truth/
51
+ │ └── manifest.json
52
+ └── evaluation/ # held-out; used for release_gate accuracy
53
+ ├── images/
54
+ ├── ground_truth/
55
+ └── manifest.json
56
+ ```
57
+
58
+ Build with non-overlapping FUNSD IDs and different synthetic seeds:
59
+
60
+ ```bash
61
+ python eval_dataset/build_release_set.py --clean \
62
+ --output-root eval_dataset/calibration \
63
+ --funsd-ids 0,1 --generator-seed 20260720 \
64
+ --funsd-revision <SHA> --latin-font <PATH> --cjk-font <PATH>
65
+
66
+ python eval_dataset/build_release_set.py --clean \
67
+ --output-root eval_dataset/evaluation \
68
+ --funsd-ids 2,3,4,5 --generator-seed 20260721 \
69
+ --funsd-revision <SHA> --latin-font <PATH> --cjk-font <PATH>
70
+ ```
71
+
72
+ `benchmarks/run_accuracy.py` reports aggregate metrics and, when
73
+ `manifest.json` lists categories, a `by_category` breakdown. Use
74
+ `--save-predictions DIR` to dump hypotheses for failure analysis.
75
+
76
  ### Ground Truth Format
77
 
78
  Each ground truth file is a plain `.txt` file containing the expected OCR output.
benchmarks/evaluate_tables.py CHANGED
@@ -11,7 +11,6 @@ from __future__ import annotations
11
 
12
  import re
13
  from html.parser import HTMLParser
14
- from pathlib import Path
15
 
16
 
17
  def _split_markdown_row(line: str) -> list[str]:
@@ -178,7 +177,7 @@ def normalize_table(table_text: str) -> dict:
178
  if re.search(r"<table\b", table_text, re.IGNORECASE):
179
  return _normalize_html_table(table_text)
180
 
181
- lines = [l.strip() for l in table_text.strip().split("\n") if l.strip()]
182
 
183
  # Filter out the Markdown header separator.
184
  data_lines = [line for line in lines if not _is_separator_row(line)]
 
11
 
12
  import re
13
  from html.parser import HTMLParser
 
14
 
15
 
16
  def _split_markdown_row(line: str) -> list[str]:
 
177
  if re.search(r"<table\b", table_text, re.IGNORECASE):
178
  return _normalize_html_table(table_text)
179
 
180
+ lines = [line.strip() for line in table_text.strip().split("\n") if line.strip()]
181
 
182
  # Filter out the Markdown header separator.
183
  data_lines = [line for line in lines if not _is_separator_row(line)]
benchmarks/rswa_validation.py CHANGED
@@ -14,13 +14,17 @@ from __future__ import annotations
14
 
15
  import argparse
16
  import json
 
17
  import platform
18
  import sys
19
  import time
 
20
  from pathlib import Path
21
 
22
  sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
23
 
 
 
24
 
25
  # Output lengths to test
26
  TEST_LENGTHS = [512, 2048, 4096, 8192, 16384]
@@ -45,7 +49,9 @@ class MinGeneratedTokensProcessor:
45
  normalized_ids = sorted({
46
  int(token_id)
47
  for token_id in eos_token_ids
48
- if isinstance(token_id, int) and not isinstance(token_id, bool)
 
 
49
  })
50
  if not normalized_ids:
51
  raise ValueError("At least one integer EOS token ID is required")
@@ -81,8 +87,12 @@ def collect_eos_token_ids(model, processor) -> list[int]:
81
  for value in values:
82
  candidates = value if isinstance(value, (list, tuple, set)) else [value]
83
  for candidate in candidates:
84
- if isinstance(candidate, int) and not isinstance(candidate, bool):
85
- normalized.add(candidate)
 
 
 
 
86
  if not normalized:
87
  raise ValueError("Could not determine EOS token IDs from model or processor")
88
  return sorted(normalized)
@@ -90,10 +100,12 @@ def collect_eos_token_ids(model, processor) -> list[int]:
90
 
91
  def detect_repetition_rate(text: str, ngram_size: int = 35, threshold: int = 3) -> float:
92
  """Detect fraction of repeated n-grams in output."""
 
 
93
  if not isinstance(ngram_size, int) or isinstance(ngram_size, bool) or ngram_size < 1:
94
  raise ValueError("ngram_size must be a positive integer")
95
- if not isinstance(threshold, int) or isinstance(threshold, bool) or threshold < 0:
96
- raise ValueError("threshold must be a non-negative integer")
97
  words = text.split()
98
  if len(words) < ngram_size:
99
  return 0.0
@@ -111,20 +123,31 @@ def analyze_cache_behavior(results: list[dict]) -> tuple[bool | None, str]:
111
  Returns ``None`` when the generated token counts do not span enough lengths
112
  to distinguish a plateau from ordinary noise or linear growth.
113
  """
114
- successful = [
115
- result
116
- for result in results
117
- if result.get("status") == "success"
118
- and result.get("tokens_generated", 0) > 0
119
- and result.get("tokens_generated_reliable", True)
120
- and result.get("memory_growth_mb") is not None
121
- ]
 
 
 
 
 
 
 
 
 
 
122
  # Collapse duplicate actual token counts; raising max_tokens is not useful
123
  # evidence when the model naturally stops at the same length each time.
124
  by_tokens: dict[int, float] = {}
125
  for result in successful:
126
  tokens = int(result["tokens_generated"])
127
- by_tokens[tokens] = max(by_tokens.get(tokens, 0.0), result["memory_growth_mb"])
 
128
  points = sorted(by_tokens.items())
129
 
130
  if len(points) < 3:
@@ -223,6 +246,8 @@ def run_rswa_validation(
223
  )
224
 
225
  baseline_memory = mx.get_active_memory() / 1e6
 
 
226
  print(f"Baseline memory: {baseline_memory:.0f} MB")
227
 
228
  results = []
@@ -232,6 +257,8 @@ def run_rswa_validation(
232
 
233
  mx.clear_cache()
234
  mem_before = mx.get_active_memory() / 1e6
 
 
235
  mx.reset_peak_memory()
236
  start = time.perf_counter()
237
 
@@ -241,11 +268,13 @@ def run_rswa_validation(
241
  if force_min_tokens is not None
242
  else None
243
  )
244
- logits_processors = (
245
- [MinGeneratedTokensProcessor(forced_minimum, eos_token_ids)]
246
- if forced_minimum is not None
247
- else None
248
- )
 
 
249
  response = generate(
250
  model, processor,
251
  prompt=prompt,
@@ -261,11 +290,20 @@ def run_rswa_validation(
261
  if isinstance(response, dict)
262
  else getattr(response, "peak_memory", None)
263
  )
 
 
 
 
 
 
 
264
  mem_after = (
265
  float(peak_memory) * 1000
266
- if peak_memory is not None and float(peak_memory) > 0
267
  else mx.get_peak_memory() / 1e6
268
  )
 
 
269
 
270
  # Extract text
271
  if isinstance(response, str):
@@ -312,14 +350,31 @@ def run_rswa_validation(
312
  tokens_reliable = False
313
 
314
  # Compute metrics
315
- tokens_generated = int(tokens_generated or 0)
 
 
 
 
 
 
316
  reported_tps = (
317
  response.get("generation_tps", 0.0)
318
  if isinstance(response, dict)
319
  else getattr(response, "generation_tps", 0.0)
320
  )
321
- tps = float(reported_tps) if reported_tps else (
322
- tokens_generated / elapsed if elapsed > 0 else 0
 
 
 
 
 
 
 
 
 
 
 
323
  )
324
  repetition = detect_repetition_rate(text)
325
  memory_growth = max(0.0, mem_after - mem_before)
@@ -406,6 +461,11 @@ def run_rswa_validation(
406
  "image_path": Path(image_path).name,
407
  "prompt": prompt,
408
  "force_min_tokens": force_min_tokens,
 
 
 
 
 
409
  "forced_eos_token_ids": eos_token_ids,
410
  "platform": platform.platform(),
411
  "processor": platform.processor(),
 
14
 
15
  import argparse
16
  import json
17
+ import math
18
  import platform
19
  import sys
20
  import time
21
+ from numbers import Integral, Real
22
  from pathlib import Path
23
 
24
  sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
25
 
26
+ from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor # noqa: E402
27
+
28
 
29
  # Output lengths to test
30
  TEST_LENGTHS = [512, 2048, 4096, 8192, 16384]
 
49
  normalized_ids = sorted({
50
  int(token_id)
51
  for token_id in eos_token_ids
52
+ if isinstance(token_id, Integral)
53
+ and not isinstance(token_id, bool)
54
+ and token_id >= 0
55
  })
56
  if not normalized_ids:
57
  raise ValueError("At least one integer EOS token ID is required")
 
87
  for value in values:
88
  candidates = value if isinstance(value, (list, tuple, set)) else [value]
89
  for candidate in candidates:
90
+ if (
91
+ isinstance(candidate, Integral)
92
+ and not isinstance(candidate, bool)
93
+ and candidate >= 0
94
+ ):
95
+ normalized.add(int(candidate))
96
  if not normalized:
97
  raise ValueError("Could not determine EOS token IDs from model or processor")
98
  return sorted(normalized)
 
100
 
101
  def detect_repetition_rate(text: str, ngram_size: int = 35, threshold: int = 3) -> float:
102
  """Detect fraction of repeated n-grams in output."""
103
+ if not isinstance(text, str):
104
+ raise TypeError("text must be a string")
105
  if not isinstance(ngram_size, int) or isinstance(ngram_size, bool) or ngram_size < 1:
106
  raise ValueError("ngram_size must be a positive integer")
107
+ if not isinstance(threshold, int) or isinstance(threshold, bool) or threshold < 1:
108
+ raise ValueError("threshold must be a positive integer")
109
  words = text.split()
110
  if len(words) < ngram_size:
111
  return 0.0
 
123
  Returns ``None`` when the generated token counts do not span enough lengths
124
  to distinguish a plateau from ordinary noise or linear growth.
125
  """
126
+ successful = []
127
+ for result in results:
128
+ if not isinstance(result, dict) or result.get("status") != "success":
129
+ continue
130
+ tokens = result.get("tokens_generated")
131
+ memory = result.get("memory_growth_mb")
132
+ if (
133
+ not isinstance(tokens, Integral)
134
+ or isinstance(tokens, bool)
135
+ or tokens <= 0
136
+ or result.get("tokens_generated_reliable", True) is not True
137
+ or not isinstance(memory, Real)
138
+ or isinstance(memory, bool)
139
+ or not math.isfinite(float(memory))
140
+ or memory < 0
141
+ ):
142
+ continue
143
+ successful.append(result)
144
  # Collapse duplicate actual token counts; raising max_tokens is not useful
145
  # evidence when the model naturally stops at the same length each time.
146
  by_tokens: dict[int, float] = {}
147
  for result in successful:
148
  tokens = int(result["tokens_generated"])
149
+ memory = float(result["memory_growth_mb"])
150
+ by_tokens[tokens] = max(by_tokens.get(tokens, 0.0), memory)
151
  points = sorted(by_tokens.items())
152
 
153
  if len(points) < 3:
 
246
  )
247
 
248
  baseline_memory = mx.get_active_memory() / 1e6
249
+ if not math.isfinite(baseline_memory) or baseline_memory < 0:
250
+ raise ValueError("MLX returned invalid active memory")
251
  print(f"Baseline memory: {baseline_memory:.0f} MB")
252
 
253
  results = []
 
257
 
258
  mx.clear_cache()
259
  mem_before = mx.get_active_memory() / 1e6
260
+ if not math.isfinite(mem_before) or mem_before < 0:
261
+ raise ValueError("MLX returned invalid active memory")
262
  mx.reset_peak_memory()
263
  start = time.perf_counter()
264
 
 
268
  if force_min_tokens is not None
269
  else None
270
  )
271
+ logits_processors = [
272
+ SlidingWindowNoRepeatNGramProcessor(35, 128)
273
+ ]
274
+ if forced_minimum is not None:
275
+ logits_processors.append(
276
+ MinGeneratedTokensProcessor(forced_minimum, eos_token_ids)
277
+ )
278
  response = generate(
279
  model, processor,
280
  prompt=prompt,
 
290
  if isinstance(response, dict)
291
  else getattr(response, "peak_memory", None)
292
  )
293
+ if peak_memory is not None and (
294
+ not isinstance(peak_memory, Real)
295
+ or isinstance(peak_memory, bool)
296
+ or not math.isfinite(float(peak_memory))
297
+ or peak_memory < 0
298
+ ):
299
+ raise ValueError("mlx-vlm returned invalid peak_memory")
300
  mem_after = (
301
  float(peak_memory) * 1000
302
+ if peak_memory is not None and peak_memory > 0
303
  else mx.get_peak_memory() / 1e6
304
  )
305
+ if not math.isfinite(mem_after) or mem_after < 0:
306
+ raise ValueError("MLX returned invalid peak memory")
307
 
308
  # Extract text
309
  if isinstance(response, str):
 
350
  tokens_reliable = False
351
 
352
  # Compute metrics
353
+ if (
354
+ not isinstance(tokens_generated, Integral)
355
+ or isinstance(tokens_generated, bool)
356
+ or not 0 <= tokens_generated <= max_tokens
357
+ ):
358
+ raise ValueError("mlx-vlm returned an invalid generation token count")
359
+ tokens_generated = int(tokens_generated)
360
  reported_tps = (
361
  response.get("generation_tps", 0.0)
362
  if isinstance(response, dict)
363
  else getattr(response, "generation_tps", 0.0)
364
  )
365
+ if isinstance(reported_tps, bool) or (
366
+ reported_tps is not None
367
+ and (
368
+ not isinstance(reported_tps, Real)
369
+ or not math.isfinite(float(reported_tps))
370
+ or reported_tps < 0
371
+ )
372
+ ):
373
+ raise ValueError("mlx-vlm returned invalid generation_tps")
374
+ tps = (
375
+ float(reported_tps)
376
+ if reported_tps
377
+ else tokens_generated / elapsed if elapsed > 0 else 0.0
378
  )
379
  repetition = detect_repetition_rate(text)
380
  memory_growth = max(0.0, mem_after - mem_before)
 
461
  "image_path": Path(image_path).name,
462
  "prompt": prompt,
463
  "force_min_tokens": force_min_tokens,
464
+ "generation_settings": {
465
+ "temperature": 0.0,
466
+ "no_repeat_ngram_size": 35,
467
+ "ngram_window": 128,
468
+ },
469
  "forced_eos_token_ids": eos_token_ids,
470
  "platform": platform.platform(),
471
  "processor": platform.processor(),
benchmarks/run_accuracy.py CHANGED
@@ -12,6 +12,7 @@ from __future__ import annotations
12
  import argparse
13
  from collections import Counter
14
  import json
 
15
  import sys
16
  import time
17
  from pathlib import Path
@@ -22,14 +23,75 @@ PROJECT_ROOT = Path(__file__).parent.parent
22
  sys.path.insert(0, str(PROJECT_ROOT))
23
  sys.path.insert(0, str(PROJECT_ROOT / "src"))
24
 
25
- from benchmarks.evaluate_cer import (
26
  compute_cer, compute_wer, compute_digit_cer,
27
  compute_cjk_cer, detect_repetition, extract_cjk_characters,
28
  )
29
- from benchmarks.normalize_output import normalize_ocr_output
30
- from benchmarks.evaluate_tables import evaluate_tables
31
- from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor
32
- from unlimited_ocr.profiles import get_profile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
 
35
  def run_accuracy_benchmark(
@@ -38,6 +100,11 @@ def run_accuracy_benchmark(
38
  prompt: str = "<image>document parsing.",
39
  max_tokens: int | None = None,
40
  profile: str = "accurate",
 
 
 
 
 
41
  ) -> dict:
42
  """Run accuracy benchmark on evaluation dataset.
43
 
@@ -60,6 +127,12 @@ def run_accuracy_benchmark(
60
  token_limit = generation_profile.max_tokens if max_tokens is None else max_tokens
61
  if not isinstance(token_limit, int) or isinstance(token_limit, bool) or token_limit < 1:
62
  raise ValueError("max_tokens must be a positive integer")
 
 
 
 
 
 
63
 
64
  # Collect image files
65
  image_files = sorted(
@@ -97,12 +170,85 @@ def run_accuracy_benchmark(
97
  (image_file, gt_dir / f"{image_file.stem}.txt")
98
  for image_file in image_files
99
  ]
100
-
101
- # Import and load only after validating the dataset and generation options.
102
- from mlx_vlm import load, generate
 
103
 
104
  print(f"Loading model: {model_path}")
105
- model, processor = load(model_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  results = []
108
  total_time = 0.0
@@ -112,20 +258,28 @@ def run_accuracy_benchmark(
112
 
113
  # Run inference
114
  start = time.perf_counter()
115
- response = generate(
116
- model, processor,
117
- prompt=prompt,
118
- image=[str(img_file)],
119
- max_tokens=token_limit,
120
- temperature=generation_profile.temperature,
121
- top_p=generation_profile.top_p,
122
- repetition_penalty=generation_profile.repetition_penalty,
123
- logits_processors=[SlidingWindowNoRepeatNGramProcessor(
124
- generation_profile.no_repeat_ngram_size,
125
- 128,
126
- )],
127
- verbose=False,
128
- )
 
 
 
 
 
 
 
 
129
  elapsed = time.perf_counter() - start
130
  total_time += elapsed
131
 
@@ -135,7 +289,10 @@ def run_accuracy_benchmark(
135
  elif hasattr(response, "text"):
136
  hypothesis = response.text
137
  else:
138
- hypothesis = str(response)
 
 
 
139
  hypothesis = normalize_ocr_output(hypothesis)
140
 
141
  # Compute metrics
@@ -151,8 +308,10 @@ def run_accuracy_benchmark(
151
  else None
152
  )
153
 
 
154
  result = {
155
  "file": img_file.name,
 
156
  "cer": cer,
157
  "wer": wer,
158
  "digit_cer": digit_cer,
@@ -169,6 +328,12 @@ def run_accuracy_benchmark(
169
  }
170
  results.append(result)
171
 
 
 
 
 
 
 
172
  print(f" [{i+1}/{len(evaluation_files)}] {img_file.name}: CER={cer:.4f} "
173
  f"DigitCER={digit_cer:.4f} ({elapsed:.1f}s)")
174
 
@@ -177,19 +342,30 @@ def run_accuracy_benchmark(
177
  digit_scores = [r["digit_cer"] for r in results if r["ref_digit_count"] > 0]
178
  cjk_scores = [r["cjk_cer"] for r in results if r["ref_cjk_count"] > 0]
179
  table_scores = [r["table_score"] for r in results if r["table_score"] is not None]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  summary = {
181
  "model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
 
 
182
  "prompt": prompt,
183
  "max_tokens": token_limit,
184
  "profile": profile,
185
- "generation_settings": {
186
- "temperature": generation_profile.temperature,
187
- "top_p": generation_profile.top_p,
188
- "repetition_penalty": generation_profile.repetition_penalty,
189
- "no_repeat_ngram_size": generation_profile.no_repeat_ngram_size,
190
- "ngram_window": 128,
191
- },
192
- "num_images": len(image_files),
193
  "num_samples": len(results),
194
  "total_time_seconds": total_time,
195
  "mean_cer": float(np.mean([r["cer"] for r in results])) if results else 0,
@@ -201,6 +377,7 @@ def run_accuracy_benchmark(
201
  "num_table_samples": len(table_scores),
202
  "mean_table_score": float(np.mean(table_scores)) if table_scores else None,
203
  "mean_repetition_rate": float(np.mean([r["repetition_rate"] for r in results])) if results else 0,
 
204
  "per_file": results,
205
  }
206
 
@@ -217,6 +394,20 @@ def main():
217
  help="Override the selected profile's token limit")
218
  parser.add_argument("--profile", default="accurate",
219
  choices=["accurate", "fast", "long-document", "plain-text", "markdown"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  args = parser.parse_args()
221
 
222
  print("=" * 60)
@@ -229,11 +420,19 @@ def main():
229
  prompt=args.prompt,
230
  max_tokens=args.max_tokens,
231
  profile=args.profile,
 
 
 
 
 
232
  )
233
 
234
  # Save results
235
  args.output.parent.mkdir(parents=True, exist_ok=True)
236
- args.output.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8")
 
 
 
237
 
238
  def metric(value):
239
  return "N/A" if value is None else f"{value:.4f}"
@@ -242,6 +441,15 @@ def main():
242
  print(f"Results: CER={results['mean_cer']:.4f} | "
243
  f"DigitCER={metric(results['mean_digit_cer'])} | "
244
  f"CJK={metric(results['mean_cjk_cer'])}")
 
 
 
 
 
 
 
 
 
245
  print(f"Saved to: {args.output}")
246
 
247
 
 
12
  import argparse
13
  from collections import Counter
14
  import json
15
+ import os
16
  import sys
17
  import time
18
  from pathlib import Path
 
23
  sys.path.insert(0, str(PROJECT_ROOT))
24
  sys.path.insert(0, str(PROJECT_ROOT / "src"))
25
 
26
+ from benchmarks.evaluate_cer import ( # noqa: E402
27
  compute_cer, compute_wer, compute_digit_cer,
28
  compute_cjk_cer, detect_repetition, extract_cjk_characters,
29
  )
30
+ from benchmarks.normalize_output import normalize_ocr_output # noqa: E402
31
+ from benchmarks.evaluate_tables import evaluate_tables # noqa: E402
32
+ from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor # noqa: E402
33
+ from unlimited_ocr.profiles import get_profile # noqa: E402
34
+
35
+
36
+ def load_category_map(eval_dir: Path) -> dict[str, str]:
37
+ """Map image filenames to categories from ``manifest.json`` when present."""
38
+ manifest_path = Path(eval_dir) / "manifest.json"
39
+ if not manifest_path.is_file():
40
+ return {}
41
+ try:
42
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
43
+ except (OSError, UnicodeError, json.JSONDecodeError):
44
+ return {}
45
+ samples = manifest.get("samples")
46
+ if not isinstance(samples, list):
47
+ return {}
48
+ mapping: dict[str, str] = {}
49
+ for sample in samples:
50
+ if not isinstance(sample, dict):
51
+ continue
52
+ category = sample.get("category")
53
+ image = sample.get("image")
54
+ if not isinstance(category, str) or not category or not isinstance(image, str):
55
+ continue
56
+ mapping[Path(image).name] = category
57
+ return mapping
58
+
59
+
60
+ def aggregate_by_category(per_file: list[dict]) -> dict[str, dict]:
61
+ """Aggregate CER-family metrics by category label on each per-file row."""
62
+ buckets: dict[str, list[dict]] = {}
63
+ for row in per_file:
64
+ if not isinstance(row, dict):
65
+ continue
66
+ category = row.get("category")
67
+ if not isinstance(category, str) or not category:
68
+ category = "uncategorized"
69
+ buckets.setdefault(category, []).append(row)
70
+
71
+ summary: dict[str, dict] = {}
72
+ for category, rows in sorted(buckets.items()):
73
+ digit_scores = [r["digit_cer"] for r in rows if r.get("ref_digit_count", 0) > 0]
74
+ cjk_scores = [r["cjk_cer"] for r in rows if r.get("ref_cjk_count", 0) > 0]
75
+ table_scores = [r["table_score"] for r in rows if r.get("table_score") is not None]
76
+ summary[category] = {
77
+ "num_samples": len(rows),
78
+ "mean_cer": (
79
+ sum(r["cer"] for r in rows) / len(rows) if rows else None
80
+ ),
81
+ "mean_digit_cer": (
82
+ sum(digit_scores) / len(digit_scores) if digit_scores else None
83
+ ),
84
+ "mean_cjk_cer": (
85
+ sum(cjk_scores) / len(cjk_scores) if cjk_scores else None
86
+ ),
87
+ "mean_table_score": (
88
+ sum(table_scores) / len(table_scores) if table_scores else None
89
+ ),
90
+ "num_digit_samples": len(digit_scores),
91
+ "num_cjk_samples": len(cjk_scores),
92
+ "num_table_samples": len(table_scores),
93
+ }
94
+ return summary
95
 
96
 
97
  def run_accuracy_benchmark(
 
100
  prompt: str = "<image>document parsing.",
101
  max_tokens: int | None = None,
102
  profile: str = "accurate",
103
+ backend: str = "mlx",
104
+ vllm_base_url: str = "http://127.0.0.1:8000/v1",
105
+ vllm_api_key: str | None = None,
106
+ served_revision: str | None = None,
107
+ save_predictions: Path | None = None,
108
  ) -> dict:
109
  """Run accuracy benchmark on evaluation dataset.
110
 
 
127
  token_limit = generation_profile.max_tokens if max_tokens is None else max_tokens
128
  if not isinstance(token_limit, int) or isinstance(token_limit, bool) or token_limit < 1:
129
  raise ValueError("max_tokens must be a positive integer")
130
+ if backend not in {"mlx", "vllm"}:
131
+ raise ValueError("backend must be one of: mlx, vllm")
132
+ if served_revision is not None and (
133
+ not isinstance(served_revision, str) or not served_revision.strip()
134
+ ):
135
+ raise ValueError("served_revision must be a non-empty string or None")
136
 
137
  # Collect image files
138
  image_files = sorted(
 
170
  (image_file, gt_dir / f"{image_file.stem}.txt")
171
  for image_file in image_files
172
  ]
173
+ category_map = load_category_map(eval_dir)
174
+ if save_predictions is not None:
175
+ save_predictions = Path(save_predictions)
176
+ save_predictions.mkdir(parents=True, exist_ok=True)
177
 
178
  print(f"Loading model: {model_path}")
179
+ if backend == "mlx":
180
+ # Import only after validating the dataset and generation options.
181
+ from mlx_vlm import generate as mlx_generate, load
182
+
183
+ model, processor = load(model_path, revision=served_revision)
184
+ return _evaluate_accuracy_files(
185
+ model_path=model_path,
186
+ evaluation_files=evaluation_files,
187
+ prompt=prompt,
188
+ token_limit=token_limit,
189
+ profile=profile,
190
+ backend=backend,
191
+ served_revision=served_revision,
192
+ generation_profile=generation_profile,
193
+ model=model,
194
+ processor=processor,
195
+ generate=mlx_generate,
196
+ engine=None,
197
+ category_map=category_map,
198
+ save_predictions=save_predictions,
199
+ )
200
+
201
+ from unlimited_ocr.vllm_backend import VLLMEngine, VLLMEngineConfig
202
+
203
+ engine = VLLMEngine(
204
+ VLLMEngineConfig(
205
+ model_path=model_path,
206
+ base_url=vllm_base_url,
207
+ api_key=vllm_api_key,
208
+ max_tokens=token_limit,
209
+ profile=profile,
210
+ )
211
+ )
212
+ try:
213
+ engine.ensure_ready()
214
+ return _evaluate_accuracy_files(
215
+ model_path=model_path,
216
+ evaluation_files=evaluation_files,
217
+ prompt=prompt,
218
+ token_limit=token_limit,
219
+ profile=profile,
220
+ backend=backend,
221
+ served_revision=served_revision,
222
+ generation_profile=generation_profile,
223
+ model=None,
224
+ processor=None,
225
+ generate=None,
226
+ engine=engine,
227
+ category_map=category_map,
228
+ save_predictions=save_predictions,
229
+ )
230
+ finally:
231
+ engine.unload()
232
+
233
+
234
+ def _evaluate_accuracy_files(
235
+ *,
236
+ model_path: str,
237
+ evaluation_files: list[tuple[Path, Path]],
238
+ prompt: str,
239
+ token_limit: int,
240
+ profile: str,
241
+ backend: str,
242
+ served_revision: str | None,
243
+ generation_profile,
244
+ model,
245
+ processor,
246
+ generate,
247
+ engine,
248
+ category_map: dict[str, str] | None = None,
249
+ save_predictions: Path | None = None,
250
+ ) -> dict:
251
+ """Evaluate already-loaded MLX or vLLM inference resources."""
252
 
253
  results = []
254
  total_time = 0.0
 
258
 
259
  # Run inference
260
  start = time.perf_counter()
261
+ if backend == "mlx":
262
+ response = generate(
263
+ model, processor,
264
+ prompt=prompt,
265
+ image=[str(img_file)],
266
+ max_tokens=token_limit,
267
+ temperature=generation_profile.temperature,
268
+ top_p=generation_profile.top_p,
269
+ repetition_penalty=generation_profile.repetition_penalty,
270
+ logits_processors=[SlidingWindowNoRepeatNGramProcessor(
271
+ generation_profile.no_repeat_ngram_size,
272
+ 128,
273
+ )],
274
+ verbose=False,
275
+ )
276
+ else:
277
+ response = engine.infer(
278
+ str(img_file),
279
+ custom_prompt=prompt,
280
+ max_tokens=token_limit,
281
+ profile=profile,
282
+ )
283
  elapsed = time.perf_counter() - start
284
  total_time += elapsed
285
 
 
289
  elif hasattr(response, "text"):
290
  hypothesis = response.text
291
  else:
292
+ raise TypeError(
293
+ "Inference backend returned an unsupported accuracy response "
294
+ f"({type(response).__name__})"
295
+ )
296
  hypothesis = normalize_ocr_output(hypothesis)
297
 
298
  # Compute metrics
 
308
  else None
309
  )
310
 
311
+ category = (category_map or {}).get(img_file.name)
312
  result = {
313
  "file": img_file.name,
314
+ "category": category,
315
  "cer": cer,
316
  "wer": wer,
317
  "digit_cer": digit_cer,
 
328
  }
329
  results.append(result)
330
 
331
+ if save_predictions is not None:
332
+ (save_predictions / f"{img_file.stem}.txt").write_text(
333
+ hypothesis + ("\n" if hypothesis and not hypothesis.endswith("\n") else ""),
334
+ encoding="utf-8",
335
+ )
336
+
337
  print(f" [{i+1}/{len(evaluation_files)}] {img_file.name}: CER={cer:.4f} "
338
  f"DigitCER={digit_cer:.4f} ({elapsed:.1f}s)")
339
 
 
342
  digit_scores = [r["digit_cer"] for r in results if r["ref_digit_count"] > 0]
343
  cjk_scores = [r["cjk_cer"] for r in results if r["ref_cjk_count"] > 0]
344
  table_scores = [r["table_score"] for r in results if r["table_score"] is not None]
345
+ generation_settings = {
346
+ "temperature": generation_profile.temperature,
347
+ "top_p": generation_profile.top_p,
348
+ "repetition_penalty": generation_profile.repetition_penalty,
349
+ "no_repeat_ngram_size": generation_profile.no_repeat_ngram_size,
350
+ "ngram_window": 128,
351
+ }
352
+ if backend == "vllm":
353
+ # The official runtime recipe always uses 35, even when an MLX
354
+ # generation profile has an experimental alternate value.
355
+ generation_settings.update({
356
+ "no_repeat_ngram_size": 35,
357
+ "skip_special_tokens": False,
358
+ })
359
+ by_category = aggregate_by_category(results)
360
  summary = {
361
  "model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
362
+ "served_revision": served_revision,
363
+ "backend": backend,
364
  "prompt": prompt,
365
  "max_tokens": token_limit,
366
  "profile": profile,
367
+ "generation_settings": generation_settings,
368
+ "num_images": len(evaluation_files),
 
 
 
 
 
 
369
  "num_samples": len(results),
370
  "total_time_seconds": total_time,
371
  "mean_cer": float(np.mean([r["cer"] for r in results])) if results else 0,
 
377
  "num_table_samples": len(table_scores),
378
  "mean_table_score": float(np.mean(table_scores)) if table_scores else None,
379
  "mean_repetition_rate": float(np.mean([r["repetition_rate"] for r in results])) if results else 0,
380
+ "by_category": by_category,
381
  "per_file": results,
382
  }
383
 
 
394
  help="Override the selected profile's token limit")
395
  parser.add_argument("--profile", default="accurate",
396
  choices=["accurate", "fast", "long-document", "plain-text", "markdown"])
397
+ parser.add_argument("--backend", default="mlx", choices=["mlx", "vllm"])
398
+ parser.add_argument("--vllm-base-url", default="http://127.0.0.1:8000/v1")
399
+ parser.add_argument("--vllm-api-key", default=os.environ.get("VLLM_API_KEY"))
400
+ parser.add_argument(
401
+ "--served-revision",
402
+ default=None,
403
+ help="Revision pinned when this model was started (required for BF16 release evidence)",
404
+ )
405
+ parser.add_argument(
406
+ "--save-predictions",
407
+ type=Path,
408
+ default=None,
409
+ help="Optional directory to write normalized hypothesis text per image stem",
410
+ )
411
  args = parser.parse_args()
412
 
413
  print("=" * 60)
 
420
  prompt=args.prompt,
421
  max_tokens=args.max_tokens,
422
  profile=args.profile,
423
+ backend=args.backend,
424
+ vllm_base_url=args.vllm_base_url,
425
+ vllm_api_key=args.vllm_api_key,
426
+ served_revision=args.served_revision,
427
+ save_predictions=args.save_predictions,
428
  )
429
 
430
  # Save results
431
  args.output.parent.mkdir(parents=True, exist_ok=True)
432
+ args.output.write_text(
433
+ json.dumps(results, indent=2, ensure_ascii=False, allow_nan=False),
434
+ encoding="utf-8",
435
+ )
436
 
437
  def metric(value):
438
  return "N/A" if value is None else f"{value:.4f}"
 
441
  print(f"Results: CER={results['mean_cer']:.4f} | "
442
  f"DigitCER={metric(results['mean_digit_cer'])} | "
443
  f"CJK={metric(results['mean_cjk_cer'])}")
444
+ if results.get("by_category"):
445
+ print("By category:")
446
+ for category, stats in results["by_category"].items():
447
+ print(
448
+ f" {category}: n={stats['num_samples']} "
449
+ f"CER={metric(stats['mean_cer'])} "
450
+ f"Digit={metric(stats['mean_digit_cer'])} "
451
+ f"CJK={metric(stats['mean_cjk_cer'])}"
452
+ )
453
  print(f"Saved to: {args.output}")
454
 
455
 
benchmarks/run_performance.py CHANGED
@@ -12,10 +12,11 @@ from __future__ import annotations
12
  import argparse
13
  import importlib.metadata
14
  import json
15
- import os
16
  import platform
17
  import sys
18
  import time
 
19
  from pathlib import Path
20
 
21
  sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
@@ -83,6 +84,7 @@ def run_performance_benchmark(
83
  max_tokens: int = 4096,
84
  num_warmup: int = 1,
85
  num_runs: int = 3,
 
86
  ) -> dict:
87
  """Run performance benchmark measuring TPS, memory, and latency."""
88
  if not Path(image_path).is_file():
@@ -95,6 +97,10 @@ def run_performance_benchmark(
95
  raise ValueError("num_warmup must be a non-negative integer")
96
  if not isinstance(num_runs, int) or isinstance(num_runs, bool) or num_runs < 1:
97
  raise ValueError("num_runs must be a positive integer")
 
 
 
 
98
 
99
  from mlx_vlm import load, generate
100
 
@@ -105,7 +111,7 @@ def run_performance_benchmark(
105
 
106
  # Measure model load time
107
  load_start = time.perf_counter()
108
- model, processor = load(model_path)
109
  load_time = time.perf_counter() - load_start
110
  mem_after_load = get_memory_usage_mb()
111
 
@@ -127,6 +133,8 @@ def run_performance_benchmark(
127
 
128
  for i in range(num_runs):
129
  active_memory_before_mb = mx.get_active_memory() / 1e6
 
 
130
  mx.reset_peak_memory()
131
  start = time.perf_counter()
132
 
@@ -140,7 +148,11 @@ def run_performance_benchmark(
140
  )
141
 
142
  elapsed = time.perf_counter() - start
 
 
143
  process_peak_mb = get_memory_usage_mb()
 
 
144
 
145
  # Extract token count
146
  if isinstance(response, str):
@@ -179,24 +191,55 @@ def run_performance_benchmark(
179
  text = str(response)
180
  tokens = len(text.split())
181
  token_count_source = "whitespace estimate"
182
- tokens = int(tokens or 0)
 
 
 
 
 
 
183
 
184
  # Prefer mlx-vlm's decode-only rate; the fallback is end-to-end and is
185
  # explicitly identified in each run.
186
- reported_tps = getattr(response, "generation_tps", 0.0)
 
 
 
 
187
  if reported_tps:
 
 
 
 
 
 
 
188
  tps = float(reported_tps)
189
  tps_source = "mlx-vlm generation_tps"
190
  else:
191
  tps = tokens / elapsed if elapsed > 0 and tokens > 0 else 0
192
  tps_source = "end-to-end estimate"
193
 
194
- reported_peak_gb = float(getattr(response, "peak_memory", 0.0) or 0.0)
 
 
 
 
 
 
 
 
 
 
 
 
195
  mlx_peak_mb = (
196
  reported_peak_gb * 1000
197
  if reported_peak_gb > 0
198
  else mx.get_peak_memory() / 1e6
199
  )
 
 
200
  run_result = {
201
  "run": i + 1,
202
  "elapsed_seconds": elapsed,
@@ -225,6 +268,7 @@ def run_performance_benchmark(
225
 
226
  results = {
227
  "model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
 
228
  "image_path": Path(image_path).name,
229
  "prompt": prompt,
230
  "max_tokens": max_tokens,
@@ -252,6 +296,11 @@ def main():
252
  parser.add_argument("--max-tokens", type=int, default=4096)
253
  parser.add_argument("--warmup", type=int, default=1)
254
  parser.add_argument("--runs", type=int, default=3)
 
 
 
 
 
255
  args = parser.parse_args()
256
 
257
  print("=" * 60)
@@ -265,6 +314,7 @@ def main():
265
  max_tokens=args.max_tokens,
266
  num_warmup=args.warmup,
267
  num_runs=args.runs,
 
268
  )
269
 
270
  args.output.parent.mkdir(parents=True, exist_ok=True)
 
12
  import argparse
13
  import importlib.metadata
14
  import json
15
+ import math
16
  import platform
17
  import sys
18
  import time
19
+ from numbers import Integral, Real
20
  from pathlib import Path
21
 
22
  sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
 
84
  max_tokens: int = 4096,
85
  num_warmup: int = 1,
86
  num_runs: int = 3,
87
+ served_revision: str | None = None,
88
  ) -> dict:
89
  """Run performance benchmark measuring TPS, memory, and latency."""
90
  if not Path(image_path).is_file():
 
97
  raise ValueError("num_warmup must be a non-negative integer")
98
  if not isinstance(num_runs, int) or isinstance(num_runs, bool) or num_runs < 1:
99
  raise ValueError("num_runs must be a positive integer")
100
+ if served_revision is not None and (
101
+ not isinstance(served_revision, str) or not served_revision.strip()
102
+ ):
103
+ raise ValueError("served_revision must be a non-empty string or None")
104
 
105
  from mlx_vlm import load, generate
106
 
 
111
 
112
  # Measure model load time
113
  load_start = time.perf_counter()
114
+ model, processor = load(model_path, revision=served_revision)
115
  load_time = time.perf_counter() - load_start
116
  mem_after_load = get_memory_usage_mb()
117
 
 
133
 
134
  for i in range(num_runs):
135
  active_memory_before_mb = mx.get_active_memory() / 1e6
136
+ if not math.isfinite(active_memory_before_mb) or active_memory_before_mb < 0:
137
+ raise ValueError("MLX returned invalid active memory")
138
  mx.reset_peak_memory()
139
  start = time.perf_counter()
140
 
 
148
  )
149
 
150
  elapsed = time.perf_counter() - start
151
+ if not math.isfinite(elapsed) or elapsed <= 0:
152
+ raise ValueError("Performance timer returned an invalid elapsed time")
153
  process_peak_mb = get_memory_usage_mb()
154
+ if not math.isfinite(process_peak_mb) or process_peak_mb < 0:
155
+ raise ValueError("Process peak memory is invalid")
156
 
157
  # Extract token count
158
  if isinstance(response, str):
 
191
  text = str(response)
192
  tokens = len(text.split())
193
  token_count_source = "whitespace estimate"
194
+ if (
195
+ not isinstance(tokens, Integral)
196
+ or isinstance(tokens, bool)
197
+ or not 0 < tokens <= max_tokens
198
+ ):
199
+ raise ValueError("mlx-vlm returned an invalid generation token count")
200
+ tokens = int(tokens)
201
 
202
  # Prefer mlx-vlm's decode-only rate; the fallback is end-to-end and is
203
  # explicitly identified in each run.
204
+ reported_tps = (
205
+ response.get("generation_tps", 0.0)
206
+ if isinstance(response, dict)
207
+ else getattr(response, "generation_tps", 0.0)
208
+ )
209
  if reported_tps:
210
+ if (
211
+ not isinstance(reported_tps, Real)
212
+ or isinstance(reported_tps, bool)
213
+ or not math.isfinite(float(reported_tps))
214
+ or reported_tps <= 0
215
+ ):
216
+ raise ValueError("mlx-vlm returned invalid generation throughput")
217
  tps = float(reported_tps)
218
  tps_source = "mlx-vlm generation_tps"
219
  else:
220
  tps = tokens / elapsed if elapsed > 0 and tokens > 0 else 0
221
  tps_source = "end-to-end estimate"
222
 
223
+ raw_peak_memory = (
224
+ response.get("peak_memory", 0.0)
225
+ if isinstance(response, dict)
226
+ else getattr(response, "peak_memory", 0.0)
227
+ )
228
+ if (
229
+ not isinstance(raw_peak_memory, Real)
230
+ or isinstance(raw_peak_memory, bool)
231
+ or not math.isfinite(float(raw_peak_memory))
232
+ or raw_peak_memory < 0
233
+ ):
234
+ raise ValueError("mlx-vlm returned invalid peak memory")
235
+ reported_peak_gb = float(raw_peak_memory)
236
  mlx_peak_mb = (
237
  reported_peak_gb * 1000
238
  if reported_peak_gb > 0
239
  else mx.get_peak_memory() / 1e6
240
  )
241
+ if not math.isfinite(mlx_peak_mb) or mlx_peak_mb < 0:
242
+ raise ValueError("MLX returned invalid peak memory")
243
  run_result = {
244
  "run": i + 1,
245
  "elapsed_seconds": elapsed,
 
268
 
269
  results = {
270
  "model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
271
+ "served_revision": served_revision,
272
  "image_path": Path(image_path).name,
273
  "prompt": prompt,
274
  "max_tokens": max_tokens,
 
296
  parser.add_argument("--max-tokens", type=int, default=4096)
297
  parser.add_argument("--warmup", type=int, default=1)
298
  parser.add_argument("--runs", type=int, default=3)
299
+ parser.add_argument(
300
+ "--served-revision",
301
+ default=None,
302
+ help="Immutable model revision used for this benchmark",
303
+ )
304
  args = parser.parse_args()
305
 
306
  print("=" * 60)
 
314
  max_tokens=args.max_tokens,
315
  num_warmup=args.warmup,
316
  num_runs=args.runs,
317
+ served_revision=args.served_revision,
318
  )
319
 
320
  args.output.parent.mkdir(parents=True, exist_ok=True)
config.json CHANGED
@@ -666,11 +666,6 @@
666
  "group_size": 32,
667
  "bits": 8,
668
  "mode": "affine"
669
- },
670
- "projector.layers": {
671
- "group_size": 32,
672
- "bits": 8,
673
- "mode": "mxfp8"
674
  }
675
  },
676
  "quantization_config": {
@@ -1271,11 +1266,6 @@
1271
  "group_size": 32,
1272
  "bits": 8,
1273
  "mode": "affine"
1274
- },
1275
- "projector.layers": {
1276
- "group_size": 32,
1277
- "bits": 8,
1278
- "mode": "mxfp8"
1279
  }
1280
  },
1281
  "rm_head": false,
 
666
  "group_size": 32,
667
  "bits": 8,
668
  "mode": "affine"
 
 
 
 
 
669
  }
670
  },
671
  "quantization_config": {
 
1266
  "group_size": 32,
1267
  "bits": 8,
1268
  "mode": "affine"
 
 
 
 
 
1269
  }
1270
  },
1271
  "rm_head": false,
docs/PRD.md CHANGED
@@ -62,6 +62,8 @@ performance, provenance, and R-SWA checks pass.
62
 
63
  - Validate image/ground-truth pairs before allocating the model.
64
  - Record dataset manifest digest and category counts.
 
 
65
  - Release evaluation must include text, digits, tables, and CJK content.
66
  - Synthetic-only evaluation may be used for development, but not labelled as a
67
  public accuracy benchmark.
 
62
 
63
  - Validate image/ground-truth pairs before allocating the model.
64
  - Record dataset manifest digest and category counts.
65
+ - Use disjoint, content-addressed datasets for sensitivity/calibration and the
66
+ final release comparison; shared image hashes block publication.
67
  - Release evaluation must include text, digits, tables, and CJK content.
68
  - Synthetic-only evaluation may be used for development, but not labelled as a
69
  public accuracy benchmark.
docs/TECHNICAL_SPEC.md CHANGED
@@ -113,6 +113,14 @@ artifacts/<run_id>/
113
  provenance.json
114
  sensitivity_results.json
115
  calibration_results.json
 
 
 
 
 
 
 
 
116
  generated_precision_map.json
117
  bf16_accuracy.json
118
  reference_accuracy.json
@@ -139,7 +147,11 @@ artifacts without loading a model. Every gate records:
139
  ```
140
 
141
  `release_approved` is true only when every required gate passes. Missing or
142
- inconclusive values fail the gate.
 
 
 
 
143
 
144
  ### 2.7 Publisher
145
 
@@ -163,8 +175,11 @@ Primary orchestration command:
163
  ```bash
164
  python quantization/run_pipeline.py \
165
  --model-path reference/Unlimited-OCR \
 
166
  --reference-model sahilchachra/unlimited-ocr-mxfp8-mlx \
167
- --eval-dir eval_dataset \
 
 
168
  --output-dir models/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
169
  --artifacts-dir artifacts/ocr-aware-v1 \
170
  --image test_data/test_invoice.png
@@ -184,20 +199,25 @@ Useful modes:
184
 
185
  - `--step preflight`
186
  - `--step sensitivity`
 
187
  - `--step precision-map`
188
  - `--step convert`
189
  - `--step validate`
190
  - `--step gate`
191
  - `--step publish`
192
- - `--step all`
193
  - `--dry-run`
194
 
 
 
 
 
195
  ## 4. Provenance schema
196
 
197
  `provenance.json` includes:
198
 
199
  - UTC run ID and timestamps;
200
- - source and reference identifiers/revisions;
201
  - file SHA-256 and byte sizes;
202
  - dataset digest and category counts;
203
  - Python, macOS, MLX, mlx-vlm, and huggingface-hub versions;
@@ -220,6 +240,10 @@ Published `processor_config.json` must use:
220
  - `processor_class: UnlimitedOCRHFProcessor`;
221
  - `sft_format: unlimitedocr`.
222
 
 
 
 
 
223
  ## 6. Error handling
224
 
225
  - Never delete or overwrite an existing output model directory.
@@ -258,3 +282,45 @@ After publication:
258
  3. Load the exact remote revision with `mlx-vlm`.
259
  4. Run deterministic OCR on the smoke image.
260
  5. Record the final revision in the local release manifest and handoff report.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  provenance.json
114
  sensitivity_results.json
115
  calibration_results.json
116
+ calibration_baseline_accuracy.json
117
+ calibration_reference_performance.json
118
+ calibration_bfloat16_accuracy.json
119
+ calibration_bfloat16_performance.json
120
+ calibration_mxfp8_accuracy.json
121
+ calibration_mxfp8_performance.json
122
+ calibration_affine8_accuracy.json
123
+ calibration_affine8_performance.json
124
  generated_precision_map.json
125
  bf16_accuracy.json
126
  reference_accuracy.json
 
147
  ```
148
 
149
  `release_approved` is true only when every required gate passes. Missing or
150
+ inconclusive values fail the gate. The calibration decision is not trusted as
151
+ an assertion: the gate verifies SHA-256 records for all eight raw calibration
152
+ benchmark files, recomputes their aggregates and quality/throughput deltas, and
153
+ reselects the fastest precision that passes the fixed limits. It also rejects a
154
+ release when calibration and held-out evaluation reuse any image hash.
155
 
156
  ### 2.7 Publisher
157
 
 
175
  ```bash
176
  python quantization/run_pipeline.py \
177
  --model-path reference/Unlimited-OCR \
178
+ --source-revision SOURCE_COMMIT_SHA \
179
  --reference-model sahilchachra/unlimited-ocr-mxfp8-mlx \
180
+ --reference-revision REFERENCE_COMMIT_SHA \
181
+ --calibration-dir datasets/selection \
182
+ --eval-dir datasets/held-out-release \
183
  --output-dir models/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
184
  --artifacts-dir artifacts/ocr-aware-v1 \
185
  --image test_data/test_invoice.png
 
199
 
200
  - `--step preflight`
201
  - `--step sensitivity`
202
+ - `--step calibrate` — interim map, three LM-head converts, select affine/mxfp8/bf16 head
203
  - `--step precision-map`
204
  - `--step convert`
205
  - `--step validate`
206
  - `--step gate`
207
  - `--step publish`
208
+ - `--step all` (runs the full order above)
209
  - `--dry-run`
210
 
211
+ `calibrate` is required before `precision-map` for schema-3 releases: it writes
212
+ `calibration_results.json` plus the eight raw `calibration_*_{accuracy,performance}.json`
213
+ evidence files on the **calibration** dataset only.
214
+
215
  ## 4. Provenance schema
216
 
217
  `provenance.json` includes:
218
 
219
  - UTC run ID and timestamps;
220
+ - source and reference identifiers plus immutable 40-character commit SHAs;
221
  - file SHA-256 and byte sizes;
222
  - dataset digest and category counts;
223
  - Python, macOS, MLX, mlx-vlm, and huggingface-hub versions;
 
240
  - `processor_class: UnlimitedOCRHFProcessor`;
241
  - `sft_format: unlimitedocr`.
242
 
243
+ If `tokenizer_config.json` is present, it must use the same
244
+ `processor_class: UnlimitedOCRHFProcessor` (convert normalization rewrites both
245
+ files so mlx-vlm and Hub-style loaders do not disagree).
246
+
247
  ## 6. Error handling
248
 
249
  - Never delete or overwrite an existing output model directory.
 
282
  3. Load the exact remote revision with `mlx-vlm`.
283
  4. Run deterministic OCR on the smoke image.
284
  5. Record the final revision in the local release manifest and handoff report.
285
+
286
+ ## 9. Schema-3 re-release checklist (MLX Hub)
287
+
288
+ The publisher requires `MLX_RELEASE_SCHEMA_VERSION = 3`. A legacy
289
+ `artifacts/ocr-aware-v1/release_manifest.json` with `schema_version: 1` cannot
290
+ be re-uploaded without a full re-gate.
291
+
292
+ 1. Build **disjoint** calibration and evaluation trees (no shared FUNSD IDs):
293
+
294
+ ```bash
295
+ python eval_dataset/build_release_set.py --clean \
296
+ --output-root eval_dataset/calibration \
297
+ --funsd-ids 0,1 --generator-seed 20260720 \
298
+ --funsd-revision <FUNSD_SHA> --latin-font <PATH> --cjk-font <PATH>
299
+
300
+ python eval_dataset/build_release_set.py --clean \
301
+ --output-root eval_dataset/evaluation \
302
+ --funsd-ids 2,3,4,5 --generator-seed 20260721 \
303
+ --funsd-revision <FUNSD_SHA> --latin-font <PATH> --cjk-font <PATH>
304
+ ```
305
+
306
+ 2. Run the OCR-aware pipeline / release gate with
307
+ `--calibration-dir eval_dataset/calibration` and
308
+ `--eval-dir eval_dataset/evaluation` (schema 3, all 23 gates).
309
+ 3. Confirm convert normalization rewrote both processor and tokenizer
310
+ `processor_class` fields to `UnlimitedOCRHFProcessor`.
311
+ 4. Dry-run then publish:
312
+
313
+ ```bash
314
+ python scripts/publish_optimized_model.py \
315
+ --model-dir models/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
316
+ --manifest artifacts/<run>/release_manifest.json \
317
+ --artifacts-dir artifacts/<run> \
318
+ --dry-run
319
+ # then without --dry-run when approved
320
+ ```
321
+
322
+ 5. Post-publish smoke against the remote revision; keep evidence under
323
+ `artifacts/`.
324
+
325
+ Model-card claims must stay local-measurement only until a public protocol
326
+ (OmniDocBench / olmOCR-Bench) is run on the candidate.
docs/adr/0002-calibrated-affine8-lm-head.md CHANGED
@@ -4,6 +4,11 @@ Status: Accepted
4
  Date: 2026-07-20
5
  Amends: ADR-0001 for `language_model.lm_head` only
6
 
 
 
 
 
 
7
  ## Context
8
 
9
  The group sensitivity run showed that the vision encoder must remain BF16, while
@@ -15,7 +20,7 @@ group sensitivity could not predict:
15
  |---|---:|---:|---:|---|
16
  | BF16 | 0.231904 | 0.082224 | 271.1 | throughput failed |
17
  | MXFP8 | 0.260170 | 0.076961 | 307.4 | CER failed |
18
- | affine INT8 | 0.237384 | 0.085733 | 301.2 | all calibration gates passed |
19
 
20
  The common BF16 baseline CER was 0.239188 and digit CER was 0.087340. The
21
  Sahil-reference throughput was 312.7 tok/s. The selection procedure used the
@@ -54,7 +59,8 @@ fail-closed release design.
54
 
55
  - The checkpoint remains predominantly MXFP8 but is accurately described as a
56
  mixed MXFP8/affine8 model.
57
- - The final formal rerun reached 297.2 tok/s, 95.0% of the Sahil reference,
58
- while mean CER and digit CER remained slightly better than BF16.
 
59
  - Consumers must use a recent MLX/`mlx-vlm` version that supports per-module
60
  affine and MXFP8 quantization metadata.
 
4
  Date: 2026-07-20
5
  Amends: ADR-0001 for `language_model.lm_head` only
6
 
7
+ Evidence status: this records a historical candidate-construction decision,
8
+ not current release approval. Its calibration and evaluation reused the same
9
+ 12 samples. The schema-3 gate now requires disjoint calibration and held-out
10
+ evaluation data before this decision can approve publication.
11
+
12
  ## Context
13
 
14
  The group sensitivity run showed that the vision encoder must remain BF16, while
 
20
  |---|---:|---:|---:|---|
21
  | BF16 | 0.231904 | 0.082224 | 271.1 | throughput failed |
22
  | MXFP8 | 0.260170 | 0.076961 | 307.4 | CER failed |
23
+ | affine INT8 | 0.237384 | 0.085733 | 301.2 | development calibration passed |
24
 
25
  The common BF16 baseline CER was 0.239188 and digit CER was 0.087340. The
26
  Sahil-reference throughput was 312.7 tok/s. The selection procedure used the
 
59
 
60
  - The checkpoint remains predominantly MXFP8 but is accurately described as a
61
  mixed MXFP8/affine8 model.
62
+ - The historical same-dataset rerun reached 297.2 tok/s, 95.0% of the Sahil
63
+ reference, while mean CER and digit CER were slightly better than BF16. This
64
+ does not replace a disjoint held-out release evaluation.
65
  - Consumers must use a recent MLX/`mlx-vlm` version that supports per-module
66
  affine and MXFP8 quantization metadata.
examples/single_image.py CHANGED
@@ -14,7 +14,7 @@ print(result)
14
 
15
  # --- Markdown output ---
16
  result = pipeline.run("your_document.jpg", format="markdown", output_path="output.md")
17
- print(f"Saved to output.md")
18
 
19
  # --- With bounding boxes (grounding mode) ---
20
  result = pipeline.run("your_document.jpg", format="json", grounding=True)
 
14
 
15
  # --- Markdown output ---
16
  result = pipeline.run("your_document.jpg", format="markdown", output_path="output.md")
17
+ print("Saved to output.md")
18
 
19
  # --- With bounding boxes (grounding mode) ---
20
  result = pipeline.run("your_document.jpg", format="json", grounding=True)
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:27568517565832e42daa563f6939e66a8ebb50cce0b01a24ecebb3ea6b4d0b10
3
- size 3843160559
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d9fe11fc8d2e333000be6a0e288681231efa69282c844f7775ca40f52db02f0c
3
+ size 3845699993
model.safetensors.index.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "metadata": {
3
- "total_size": 3843066880
4
  },
5
  "weight_map": {
6
  "image_newline": "model.safetensors",
@@ -280,7 +280,6 @@
280
  "language_model.model.layers.9.self_attn.v_proj.weight": "model.safetensors",
281
  "language_model.model.norm.weight": "model.safetensors",
282
  "projector.layers.bias": "model.safetensors",
283
- "projector.layers.scales": "model.safetensors",
284
  "projector.layers.weight": "model.safetensors",
285
  "sam_model.blocks.0.attn.proj.bias": "model.safetensors",
286
  "sam_model.blocks.0.attn.proj.weight": "model.safetensors",
 
1
  {
2
  "metadata": {
3
+ "total_size": 3845606400
4
  },
5
  "weight_map": {
6
  "image_newline": "model.safetensors",
 
280
  "language_model.model.layers.9.self_attn.v_proj.weight": "model.safetensors",
281
  "language_model.model.norm.weight": "model.safetensors",
282
  "projector.layers.bias": "model.safetensors",
 
283
  "projector.layers.weight": "model.safetensors",
284
  "sam_model.blocks.0.attn.proj.bias": "model.safetensors",
285
  "sam_model.blocks.0.attn.proj.weight": "model.safetensors",
precision_map.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "vision_model": "bfloat16",
3
- "projector": "mxfp8",
4
  "language_model.model.embed_tokens": "mxfp8",
5
  "language_model.model.norm": "bfloat16",
6
  "language_model.lm_head": "affine8",
@@ -32,16 +32,16 @@
32
  "vision_encoder": {
33
  "precision": "bfloat16",
34
  "reasons": [
35
- "cer_delta=2.423686",
36
- "digit_cer_delta=1.358023",
37
  "table_score_degradation=1.000000",
38
  "protected OCR-critical group"
39
  ]
40
  },
41
  "vision_projector": {
42
- "precision": "mxfp8",
43
  "reasons": [
44
- "all measured quality deltas were non-degrading"
45
  ]
46
  },
47
  "token_embeddings": {
 
1
  {
2
  "vision_model": "bfloat16",
3
+ "projector": "bfloat16",
4
  "language_model.model.embed_tokens": "mxfp8",
5
  "language_model.model.norm": "bfloat16",
6
  "language_model.lm_head": "affine8",
 
32
  "vision_encoder": {
33
  "precision": "bfloat16",
34
  "reasons": [
35
+ "cer_delta=2.272233",
36
+ "digit_cer_delta=1.696454",
37
  "table_score_degradation=1.000000",
38
  "protected OCR-critical group"
39
  ]
40
  },
41
  "vision_projector": {
42
+ "precision": "bfloat16",
43
  "reasons": [
44
+ "protected unless all measured quality deltas are non-degrading"
45
  ]
46
  },
47
  "token_embeddings": {
preprocessing_config.json CHANGED
@@ -66,7 +66,7 @@
66
  "target_size": 1024
67
  },
68
  "table": {
69
- "description": "Sharpen + edge enhancement for grid lines and table structure",
70
  "deskew": true,
71
  "clahe": true,
72
  "binarize": false,
@@ -75,6 +75,29 @@
75
  "grayscale": false,
76
  "target_size": 1024,
77
  "clahe_clip_limit": 3.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
  },
80
  "_note": "Preprocessing is always optional. The raw input path remains available for baseline comparison. Applied steps are recorded in output metadata."
 
66
  "target_size": 1024
67
  },
68
  "table": {
69
+ "description": "Deskew + CLAHE + sharpening for grid lines and table structure",
70
  "deskew": true,
71
  "clahe": true,
72
  "binarize": false,
 
75
  "grayscale": false,
76
  "target_size": 1024,
77
  "clahe_clip_limit": 3.0
78
+ },
79
+ "adaptive-binarize": {
80
+ "description": "Detect sideways orientation and apply local Sauvola binarization",
81
+ "deskew": true,
82
+ "clahe": false,
83
+ "binarize": "sauvola",
84
+ "gamma_correction": false,
85
+ "sharpen": false,
86
+ "grayscale": false,
87
+ "target_size": 1024
88
+ },
89
+ "photo-document": {
90
+ "description": "Perspective-correct photographed pages, detect sideways orientation, then enhance contrast and lighting",
91
+ "deskew": false,
92
+ "perspective_correction": true,
93
+ "clahe": true,
94
+ "binarize": false,
95
+ "gamma_correction": true,
96
+ "sharpen": false,
97
+ "grayscale": false,
98
+ "target_size": 1024,
99
+ "clahe_clip_limit": 3.0,
100
+ "gamma_value": 1.3
101
  }
102
  },
103
  "_note": "Preprocessing is always optional. The raw input path remains available for baseline comparison. Applied steps are recorded in output metadata."
pyproject.toml CHANGED
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
4
 
5
  [project]
6
  name = "unlimited-ocr"
7
- version = "0.2.0"
8
- description = "Improved MLX inference pipeline for Unlimited-OCR one-shot long-horizon document parsing on Apple Silicon"
9
  readme = "README.md"
10
  license = "MIT"
11
  requires-python = ">=3.10"
12
  authors = [
13
  { name = "AutomatosX" },
14
  ]
15
- keywords = ["ocr", "mlx", "apple-silicon", "document-parsing", "vision-language-model"]
16
  classifiers = [
17
  "Development Status :: 4 - Beta",
18
  "Intended Audience :: Developers",
@@ -20,13 +20,10 @@ classifiers = [
20
  "Programming Language :: Python :: 3.10",
21
  "Programming Language :: Python :: 3.11",
22
  "Programming Language :: Python :: 3.12",
 
23
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
  ]
25
  dependencies = [
26
- # Unlimited-OCR support first shipped in mlx-vlm 0.6.4. Older releases
27
- # cannot load the default model used by this package.
28
- "mlx-vlm>=0.6.4",
29
- "mlx>=0.31.2",
30
  "Pillow>=10.0",
31
  "numpy",
32
  "pymupdf>=1.24",
@@ -39,6 +36,18 @@ dependencies = [
39
  ]
40
 
41
  [project.optional-dependencies]
 
 
 
 
 
 
 
 
 
 
 
 
42
  server = ["fastapi>=0.100", "uvicorn[standard]>=0.20", "python-multipart>=0.0.5"]
43
  dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "httpx>=0.24"]
44
 
 
4
 
5
  [project]
6
  name = "unlimited-ocr"
7
+ version = "0.3.0"
8
+ description = "Unlimited-OCR inference toolkit for Apple MLX and NVIDIA CUDA/vLLM"
9
  readme = "README.md"
10
  license = "MIT"
11
  requires-python = ">=3.10"
12
  authors = [
13
  { name = "AutomatosX" },
14
  ]
15
+ keywords = ["ocr", "mlx", "cuda", "vllm", "document-parsing", "vision-language-model"]
16
  classifiers = [
17
  "Development Status :: 4 - Beta",
18
  "Intended Audience :: Developers",
 
20
  "Programming Language :: Python :: 3.10",
21
  "Programming Language :: Python :: 3.11",
22
  "Programming Language :: Python :: 3.12",
23
+ "Environment :: GPU :: NVIDIA CUDA",
24
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
  ]
26
  dependencies = [
 
 
 
 
27
  "Pillow>=10.0",
28
  "numpy",
29
  "pymupdf>=1.24",
 
36
  ]
37
 
38
  [project.optional-dependencies]
39
+ mlx = [
40
+ # Unlimited-OCR support first shipped in mlx-vlm 0.6.4. Keeping MLX
41
+ # optional lets the same wheel install on Linux CUDA hosts.
42
+ "mlx-vlm>=0.6.4; platform_system == 'Darwin'",
43
+ "mlx>=0.31.2; platform_system == 'Darwin'",
44
+ ]
45
+ vllm-client = ["httpx>=0.27,<1"]
46
+ vllm-server = [
47
+ # Unlimited-OCR became a native architecture in vLLM 0.25.
48
+ "vllm>=0.25.1,<0.26; platform_system == 'Linux' and platform_machine == 'x86_64'",
49
+ "httpx>=0.27,<1",
50
+ ]
51
  server = ["fastapi>=0.100", "uvicorn[standard]>=0.20", "python-multipart>=0.0.5"]
52
  dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "httpx>=0.24"]
53
 
quantization/calibrate_precision.py CHANGED
@@ -5,14 +5,23 @@ from __future__ import annotations
5
 
6
  import argparse
7
  import json
 
8
  import sys
 
9
  from datetime import datetime, timezone
10
  from pathlib import Path
11
 
12
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
13
  sys.path.insert(0, str(PROJECT_ROOT))
14
 
15
- from quantization.release_gate import DEFAULT_THRESHOLDS, load_json_object
 
 
 
 
 
 
 
16
 
17
 
18
  SUPPORTED_HEAD_PRECISIONS = {"bfloat16", "mxfp8", "affine8"}
@@ -20,7 +29,13 @@ SUPPORTED_HEAD_PRECISIONS = {"bfloat16", "mxfp8", "affine8"}
20
 
21
  def _number(payload: dict, key: str) -> float | None:
22
  value = payload.get(key)
23
- return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
 
 
 
 
 
 
24
 
25
 
26
  def select_head_precision(
@@ -30,15 +45,34 @@ def select_head_precision(
30
  thresholds: dict | None = None,
31
  ) -> dict:
32
  """Choose the fastest experiment that passes the existing release limits."""
33
- limits = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
34
  bf16_cer = _number(bf16_accuracy, "mean_cer")
35
  bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
36
  bf16_table = _number(bf16_accuracy, "mean_table_score")
37
  reference_tps = _number(reference_performance, "mean_tps")
38
  if None in (bf16_cer, bf16_digit, bf16_table, reference_tps) or reference_tps <= 0:
39
  raise ValueError("Baseline accuracy and reference throughput must be complete")
40
- if not experiments:
41
  raise ValueError("At least one calibration experiment is required")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  evaluated = []
44
  for experiment in experiments:
@@ -46,8 +80,6 @@ def select_head_precision(
46
  precision = experiment.get("precision")
47
  accuracy = experiment.get("accuracy")
48
  performance = experiment.get("performance")
49
- if not isinstance(label, str) or not label:
50
- raise ValueError("Each calibration experiment needs a label")
51
  if precision not in SUPPORTED_HEAD_PRECISIONS:
52
  raise ValueError(f"Unsupported head precision for {label}: {precision}")
53
  if not isinstance(accuracy, dict) or not isinstance(performance, dict):
@@ -57,40 +89,59 @@ def select_head_precision(
57
  candidate_table = _number(accuracy, "mean_table_score")
58
  candidate_tps = _number(performance, "mean_tps")
59
  metrics_complete = None not in (
60
- candidate_cer, candidate_digit, candidate_table, candidate_tps
 
 
 
61
  )
62
  deltas = {
63
  "cer_vs_bf16": candidate_cer - bf16_cer if metrics_complete else None,
64
- "digit_cer_vs_bf16": candidate_digit - bf16_digit if metrics_complete else None,
65
- "table_degradation_vs_bf16": bf16_table - candidate_table if metrics_complete else None,
66
- "tps_ratio_vs_reference": candidate_tps / reference_tps if metrics_complete else None,
 
 
 
 
 
 
67
  }
68
  checks = {
69
- "cer": metrics_complete and deltas["cer_vs_bf16"] <= limits["max_cer_delta_vs_bf16"],
70
- "digit_cer": metrics_complete and deltas["digit_cer_vs_bf16"] <= limits["max_digit_cer_delta_vs_bf16"],
71
- "table_score": metrics_complete and deltas["table_degradation_vs_bf16"] <= limits["max_table_score_degradation_vs_bf16"],
72
- "throughput": metrics_complete and deltas["tps_ratio_vs_reference"] >= limits["min_tps_ratio_vs_reference"],
 
 
 
 
 
 
73
  }
74
- evaluated.append({
75
- "label": label,
76
- "precision": precision,
77
- "passed": all(checks.values()),
78
- "checks": checks,
79
- "metrics": {
80
- "mean_cer": candidate_cer,
81
- "mean_digit_cer": candidate_digit,
82
- "mean_table_score": candidate_table,
83
- "mean_tps": candidate_tps,
84
- },
85
- "deltas": deltas,
86
- })
 
 
87
 
88
  passing = [experiment for experiment in evaluated if experiment["passed"]]
89
  if not passing:
90
- raise RuntimeError("No LM-head calibration experiment passed every release limit")
 
 
91
  selected = max(passing, key=lambda experiment: experiment["metrics"]["mean_tps"])
92
  return {
93
- "schema_version": 1,
94
  "created_at": datetime.now(timezone.utc).isoformat(),
95
  "target_pattern": "language_model.lm_head",
96
  "selection_policy": "fastest candidate passing existing quality and throughput limits",
@@ -110,6 +161,14 @@ def main() -> None:
110
  parser = argparse.ArgumentParser(description=__doc__)
111
  parser.add_argument("--bf16-accuracy", required=True, type=Path)
112
  parser.add_argument("--reference-performance", required=True, type=Path)
 
 
 
 
 
 
 
 
113
  parser.add_argument(
114
  "--experiment",
115
  action="append",
@@ -119,22 +178,91 @@ def main() -> None:
119
  )
120
  parser.add_argument("--output", required=True, type=Path)
121
  args = parser.parse_args()
122
- experiments = [
123
- {
124
- "label": label,
125
- "precision": precision,
126
- "accuracy": load_json_object(accuracy_path),
127
- "performance": load_json_object(performance_path),
128
- }
129
- for label, precision, accuracy_path, performance_path in args.experiment
130
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  result = select_head_precision(
132
- load_json_object(args.bf16_accuracy),
133
- load_json_object(args.reference_performance),
134
  experiments,
135
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  args.output.parent.mkdir(parents=True, exist_ok=True)
137
- args.output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  print(json.dumps(result["selected"], ensure_ascii=False))
139
 
140
 
 
5
 
6
  import argparse
7
  import json
8
+ import math
9
  import sys
10
+ import tempfile
11
  from datetime import datetime, timezone
12
  from pathlib import Path
13
 
14
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
15
  sys.path.insert(0, str(PROJECT_ROOT))
16
 
17
+ from quantization.release_gate import ( # noqa: E402
18
+ CALIBRATION_EVIDENCE_KEYS,
19
+ dataset_manifest,
20
+ load_json_object,
21
+ sha256_file,
22
+ validate_calibration_results,
23
+ validate_release_thresholds,
24
+ )
25
 
26
 
27
  SUPPORTED_HEAD_PRECISIONS = {"bfloat16", "mxfp8", "affine8"}
 
29
 
30
  def _number(payload: dict, key: str) -> float | None:
31
  value = payload.get(key)
32
+ if (
33
+ not isinstance(value, (int, float))
34
+ or isinstance(value, bool)
35
+ or not math.isfinite(float(value))
36
+ ):
37
+ return None
38
+ return float(value)
39
 
40
 
41
  def select_head_precision(
 
45
  thresholds: dict | None = None,
46
  ) -> dict:
47
  """Choose the fastest experiment that passes the existing release limits."""
48
+ limits = validate_release_thresholds(thresholds)
49
  bf16_cer = _number(bf16_accuracy, "mean_cer")
50
  bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
51
  bf16_table = _number(bf16_accuracy, "mean_table_score")
52
  reference_tps = _number(reference_performance, "mean_tps")
53
  if None in (bf16_cer, bf16_digit, bf16_table, reference_tps) or reference_tps <= 0:
54
  raise ValueError("Baseline accuracy and reference throughput must be complete")
55
+ if not isinstance(experiments, list) or not experiments:
56
  raise ValueError("At least one calibration experiment is required")
57
+ if any(not isinstance(experiment, dict) for experiment in experiments):
58
+ raise ValueError("Each calibration experiment must be an object")
59
+ labels = [experiment.get("label") for experiment in experiments]
60
+ precisions = [experiment.get("precision") for experiment in experiments]
61
+ if any(
62
+ not isinstance(label, str) or not label.strip() or label.strip() != label
63
+ for label in labels
64
+ ):
65
+ raise ValueError("Each calibration experiment needs a normalized label")
66
+ if any(not isinstance(precision, str) for precision in precisions):
67
+ raise ValueError("Each calibration experiment needs a precision")
68
+ if len(set(labels)) != len(labels):
69
+ raise ValueError("Calibration experiment labels must be unique")
70
+ if len(set(precisions)) != len(precisions):
71
+ raise ValueError("Calibration experiment precisions must be unique")
72
+ if set(precisions) != SUPPORTED_HEAD_PRECISIONS:
73
+ raise ValueError(
74
+ "Calibration requires exactly one experiment for each supported precision"
75
+ )
76
 
77
  evaluated = []
78
  for experiment in experiments:
 
80
  precision = experiment.get("precision")
81
  accuracy = experiment.get("accuracy")
82
  performance = experiment.get("performance")
 
 
83
  if precision not in SUPPORTED_HEAD_PRECISIONS:
84
  raise ValueError(f"Unsupported head precision for {label}: {precision}")
85
  if not isinstance(accuracy, dict) or not isinstance(performance, dict):
 
89
  candidate_table = _number(accuracy, "mean_table_score")
90
  candidate_tps = _number(performance, "mean_tps")
91
  metrics_complete = None not in (
92
+ candidate_cer,
93
+ candidate_digit,
94
+ candidate_table,
95
+ candidate_tps,
96
  )
97
  deltas = {
98
  "cer_vs_bf16": candidate_cer - bf16_cer if metrics_complete else None,
99
+ "digit_cer_vs_bf16": candidate_digit - bf16_digit
100
+ if metrics_complete
101
+ else None,
102
+ "table_degradation_vs_bf16": bf16_table - candidate_table
103
+ if metrics_complete
104
+ else None,
105
+ "tps_ratio_vs_reference": candidate_tps / reference_tps
106
+ if metrics_complete
107
+ else None,
108
  }
109
  checks = {
110
+ "cer": metrics_complete
111
+ and deltas["cer_vs_bf16"] <= limits["max_cer_delta_vs_bf16"],
112
+ "digit_cer": metrics_complete
113
+ and deltas["digit_cer_vs_bf16"] <= limits["max_digit_cer_delta_vs_bf16"],
114
+ "table_score": metrics_complete
115
+ and deltas["table_degradation_vs_bf16"]
116
+ <= limits["max_table_score_degradation_vs_bf16"],
117
+ "throughput": metrics_complete
118
+ and deltas["tps_ratio_vs_reference"]
119
+ >= limits["min_tps_ratio_vs_reference"],
120
  }
121
+ evaluated.append(
122
+ {
123
+ "label": label,
124
+ "precision": precision,
125
+ "passed": all(checks.values()),
126
+ "checks": checks,
127
+ "metrics": {
128
+ "mean_cer": candidate_cer,
129
+ "mean_digit_cer": candidate_digit,
130
+ "mean_table_score": candidate_table,
131
+ "mean_tps": candidate_tps,
132
+ },
133
+ "deltas": deltas,
134
+ }
135
+ )
136
 
137
  passing = [experiment for experiment in evaluated if experiment["passed"]]
138
  if not passing:
139
+ raise RuntimeError(
140
+ "No LM-head calibration experiment passed every release limit"
141
+ )
142
  selected = max(passing, key=lambda experiment: experiment["metrics"]["mean_tps"])
143
  return {
144
+ "schema_version": 3,
145
  "created_at": datetime.now(timezone.utc).isoformat(),
146
  "target_pattern": "language_model.lm_head",
147
  "selection_policy": "fastest candidate passing existing quality and throughput limits",
 
161
  parser = argparse.ArgumentParser(description=__doc__)
162
  parser.add_argument("--bf16-accuracy", required=True, type=Path)
163
  parser.add_argument("--reference-performance", required=True, type=Path)
164
+ parser.add_argument(
165
+ "--calibration-dir",
166
+ required=True,
167
+ type=Path,
168
+ help="Selection-only dataset used for every calibration accuracy run",
169
+ )
170
+ parser.add_argument("--source-revision", required=True)
171
+ parser.add_argument("--reference-revision", required=True)
172
  parser.add_argument(
173
  "--experiment",
174
  action="append",
 
178
  )
179
  parser.add_argument("--output", required=True, type=Path)
180
  args = parser.parse_args()
181
+ bf16_accuracy = load_json_object(args.bf16_accuracy)
182
+ reference_performance = load_json_object(args.reference_performance)
183
+ experiments = []
184
+ calibration_dataset = dataset_manifest(args.calibration_dir)
185
+ evidence = {
186
+ "calibration_baseline_accuracy": bf16_accuracy,
187
+ "calibration_reference_performance": reference_performance,
188
+ }
189
+ evidence_paths: dict[str, Path] = {
190
+ "calibration_baseline_accuracy": args.bf16_accuracy,
191
+ "calibration_reference_performance": args.reference_performance,
192
+ }
193
+ for label, precision, accuracy_path, performance_path in args.experiment:
194
+ if precision not in CALIBRATION_EVIDENCE_KEYS:
195
+ raise ValueError(f"Unsupported calibration precision: {precision}")
196
+ accuracy_key, performance_key = CALIBRATION_EVIDENCE_KEYS[precision]
197
+ accuracy_path = Path(accuracy_path)
198
+ performance_path = Path(performance_path)
199
+ evidence[accuracy_key] = load_json_object(accuracy_path)
200
+ evidence[performance_key] = load_json_object(performance_path)
201
+ evidence_paths[accuracy_key] = accuracy_path
202
+ evidence_paths[performance_key] = performance_path
203
+ experiments.append(
204
+ {
205
+ "label": label,
206
+ "precision": precision,
207
+ "accuracy": evidence[accuracy_key],
208
+ "performance": evidence[performance_key],
209
+ }
210
+ )
211
  result = select_head_precision(
212
+ bf16_accuracy,
213
+ reference_performance,
214
  experiments,
215
  )
216
+ expected_input_keys = {
217
+ "calibration_baseline_accuracy",
218
+ "calibration_reference_performance",
219
+ *(name for pair in CALIBRATION_EVIDENCE_KEYS.values() for name in pair),
220
+ }
221
+ if set(evidence_paths) != expected_input_keys:
222
+ raise ValueError("Calibration requires one unique evidence pair per precision")
223
+ resolved_paths = [path.resolve(strict=True) for path in evidence_paths.values()]
224
+ if len(set(resolved_paths)) != len(resolved_paths):
225
+ raise ValueError("Calibration input artifacts must be distinct files")
226
+ result["input_artifacts"] = {
227
+ name: {
228
+ "filename": path.name,
229
+ "size": path.stat().st_size,
230
+ "sha256": sha256_file(path),
231
+ }
232
+ for name, path in evidence_paths.items()
233
+ }
234
+ result["dataset"] = calibration_dataset
235
+ validate_calibration_results(
236
+ result,
237
+ bf16_accuracy=bf16_accuracy,
238
+ reference_performance=reference_performance,
239
+ calibration_dataset=calibration_dataset,
240
+ source_revision=args.source_revision,
241
+ reference_revision=args.reference_revision,
242
+ evidence=evidence,
243
+ evidence_paths=evidence_paths,
244
+ )
245
+ if args.output.is_symlink() or (args.output.exists() and not args.output.is_file()):
246
+ raise ValueError(f"Output must be a regular file: {args.output}")
247
  args.output.parent.mkdir(parents=True, exist_ok=True)
248
+ temporary_path = None
249
+ try:
250
+ with tempfile.NamedTemporaryFile(
251
+ mode="w",
252
+ encoding="utf-8",
253
+ dir=args.output.parent,
254
+ prefix=f".{args.output.name}.",
255
+ suffix=".tmp",
256
+ delete=False,
257
+ ) as temporary:
258
+ temporary_path = Path(temporary.name)
259
+ json.dump(result, temporary, indent=2, ensure_ascii=False, allow_nan=False)
260
+ temporary.write("\n")
261
+ temporary_path.replace(args.output)
262
+ temporary_path = None
263
+ finally:
264
+ if temporary_path is not None:
265
+ temporary_path.unlink(missing_ok=True)
266
  print(json.dumps(result["selected"], ensure_ascii=False))
267
 
268
 
quantization/layer_sensitivity.py CHANGED
@@ -29,12 +29,13 @@ PROJECT_ROOT = Path(__file__).parent.parent
29
  sys.path.insert(0, str(PROJECT_ROOT))
30
  sys.path.insert(0, str(PROJECT_ROOT / "src"))
31
 
32
- from benchmarks.evaluate_cer import compute_cer as _compute_cer
33
- from benchmarks.evaluate_cer import compute_digit_cer as _compute_digit_cer
34
- from benchmarks.evaluate_tables import evaluate_tables
35
- from benchmarks.normalize_output import normalize_ocr_output
36
- from quantization.mixed_precision_convert import matches_pattern
37
- from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor
 
38
 
39
 
40
  # Quantizable MLX module groups to test independently. Normalization and the
@@ -239,6 +240,11 @@ def main():
239
  default=None,
240
  help="Public source identifier recorded in results (defaults to repo ID or local name)",
241
  )
 
 
 
 
 
242
  parser.add_argument("--eval-dir", required=True, type=Path,
243
  help="Evaluation directory with images/ and ground_truth/")
244
  parser.add_argument("--output", type=Path, default=Path("sensitivity_results.json"),
@@ -259,6 +265,7 @@ def main():
259
 
260
  # Fail before importing MLX or allocating the model for dataset mistakes.
261
  collect_evaluation_files(args.eval_dir, args.prompt, args.max_tokens)
 
262
 
263
  print("=" * 60)
264
  print("Layer Sensitivity Analysis for Unlimited-OCR MLX")
@@ -270,12 +277,12 @@ def main():
270
  from mlx_vlm.utils import load_config
271
  import mlx.core as mx
272
 
273
- base_config = load_config(args.model_path)
274
  if base_config.get("quantization") or base_config.get("quantization_config"):
275
  raise ValueError(
276
  "Layer sensitivity requires an unquantized BF16 source model"
277
  )
278
- model, processor = load(args.model_path)
279
 
280
  # Step 2: Evaluate baseline
281
  print("\n[2/3] Evaluating BF16 baseline...")
@@ -306,7 +313,9 @@ def main():
306
  "model_path": args.source_id or (
307
  Path(args.model_path).name if Path(args.model_path).is_dir() else args.model_path
308
  ),
 
309
  "eval_dir": args.eval_dir.name,
 
310
  "prompt": args.prompt,
311
  "max_tokens": args.max_tokens,
312
  "groups_tested": args.groups,
@@ -318,7 +327,10 @@ def main():
318
  group_model = None
319
  group_processor = None
320
  try:
321
- group_model, group_processor = load(args.model_path)
 
 
 
322
  matched_modules = quantize_layer_group(
323
  group_model,
324
  dict(base_config),
 
29
  sys.path.insert(0, str(PROJECT_ROOT))
30
  sys.path.insert(0, str(PROJECT_ROOT / "src"))
31
 
32
+ from benchmarks.evaluate_cer import compute_cer as _compute_cer # noqa: E402
33
+ from benchmarks.evaluate_cer import compute_digit_cer as _compute_digit_cer # noqa: E402
34
+ from benchmarks.evaluate_tables import evaluate_tables # noqa: E402
35
+ from benchmarks.normalize_output import normalize_ocr_output # noqa: E402
36
+ from quantization.mixed_precision_convert import matches_pattern # noqa: E402
37
+ from quantization.release_gate import dataset_manifest # noqa: E402
38
+ from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor # noqa: E402
39
 
40
 
41
  # Quantizable MLX module groups to test independently. Normalization and the
 
240
  default=None,
241
  help="Public source identifier recorded in results (defaults to repo ID or local name)",
242
  )
243
+ parser.add_argument(
244
+ "--source-revision",
245
+ default=None,
246
+ help="Immutable source commit used for remote loading and provenance",
247
+ )
248
  parser.add_argument("--eval-dir", required=True, type=Path,
249
  help="Evaluation directory with images/ and ground_truth/")
250
  parser.add_argument("--output", type=Path, default=Path("sensitivity_results.json"),
 
265
 
266
  # Fail before importing MLX or allocating the model for dataset mistakes.
267
  collect_evaluation_files(args.eval_dir, args.prompt, args.max_tokens)
268
+ dataset = dataset_manifest(args.eval_dir)
269
 
270
  print("=" * 60)
271
  print("Layer Sensitivity Analysis for Unlimited-OCR MLX")
 
277
  from mlx_vlm.utils import load_config
278
  import mlx.core as mx
279
 
280
+ base_config = load_config(args.model_path, revision=args.source_revision)
281
  if base_config.get("quantization") or base_config.get("quantization_config"):
282
  raise ValueError(
283
  "Layer sensitivity requires an unquantized BF16 source model"
284
  )
285
+ model, processor = load(args.model_path, revision=args.source_revision)
286
 
287
  # Step 2: Evaluate baseline
288
  print("\n[2/3] Evaluating BF16 baseline...")
 
313
  "model_path": args.source_id or (
314
  Path(args.model_path).name if Path(args.model_path).is_dir() else args.model_path
315
  ),
316
+ "source_revision": args.source_revision,
317
  "eval_dir": args.eval_dir.name,
318
+ "dataset": dataset,
319
  "prompt": args.prompt,
320
  "max_tokens": args.max_tokens,
321
  "groups_tested": args.groups,
 
327
  group_model = None
328
  group_processor = None
329
  try:
330
+ group_model, group_processor = load(
331
+ args.model_path,
332
+ revision=args.source_revision,
333
+ )
334
  matched_modules = quantize_layer_group(
335
  group_model,
336
  dict(base_config),
quantization/mixed_precision_convert.py CHANGED
@@ -104,6 +104,28 @@ def normalize_unlimited_ocr_metadata(model_dir: Path) -> None:
104
  encoding="utf-8",
105
  )
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  def load_precision_map(path: Path) -> dict:
109
  """Load the precision map JSON."""
@@ -178,6 +200,8 @@ def convert_model(
178
  precision_map: dict,
179
  output_dir: Path,
180
  verbose: bool = True,
 
 
181
  ) -> Path:
182
  """Apply mixed-precision quantization according to the precision map.
183
 
@@ -211,7 +235,7 @@ def convert_model(
211
  return False
212
 
213
  if verbose:
214
- print(f"\nPrecision map summary:")
215
  for pattern, precision in precision_map.items():
216
  if not pattern.startswith("_"):
217
  print(f" {pattern}: {precision}")
@@ -224,6 +248,7 @@ def convert_model(
224
  convert(
225
  hf_path=model_path,
226
  mlx_path=str(staging_dir),
 
227
  quantize=True,
228
  q_group_size=32,
229
  q_bits=8,
@@ -255,7 +280,7 @@ def convert_model(
255
  normalize_unlimited_ocr_metadata(staging_dir)
256
 
257
  (staging_dir / "precision_map.json").write_text(
258
- json.dumps(precision_map, indent=2),
259
  encoding="utf-8",
260
  )
261
  (staging_dir / "quantization_summary.json").write_text(
@@ -266,6 +291,7 @@ def convert_model(
266
  "source_model": (
267
  Path(model_path).name if Path(model_path).is_dir() else model_path
268
  ),
 
269
  "precision_map_sha256": _json_digest(precision_map),
270
  "quantized_module_count": len(set(quantized_modules)),
271
  "quantized_precision_counts": {
@@ -298,6 +324,11 @@ def main():
298
  )
299
  parser.add_argument("--model-path", required=True,
300
  help="Source model (baidu/Unlimited-OCR or local path)")
 
 
 
 
 
301
  parser.add_argument("--precision-map", type=Path,
302
  default=Path(__file__).parent / "precision_map.json",
303
  help="Path to precision_map.json")
@@ -316,7 +347,13 @@ def main():
316
  print("=" * 60)
317
 
318
  precision_map = load_precision_map(args.precision_map)
319
- convert_model(args.model_path, precision_map, args.output_dir, args.verbose)
 
 
 
 
 
 
320
 
321
 
322
  if __name__ == "__main__":
 
104
  encoding="utf-8",
105
  )
106
 
107
+ # mlx-vlm convert may stamp a different processor_class on tokenizer_config.
108
+ # Keep it aligned so Hub/transformers-style loaders and mlx-vlm agree.
109
+ tokenizer_config_path = model_dir / "tokenizer_config.json"
110
+ if tokenizer_config_path.is_file():
111
+ try:
112
+ tokenizer_config = json.loads(
113
+ tokenizer_config_path.read_text(encoding="utf-8")
114
+ )
115
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
116
+ raise ValueError(
117
+ "Converted model has invalid tokenizer_config.json"
118
+ ) from exc
119
+ if not isinstance(tokenizer_config, dict):
120
+ raise ValueError(
121
+ "Converted tokenizer_config.json must contain a JSON object"
122
+ )
123
+ tokenizer_config["processor_class"] = "UnlimitedOCRHFProcessor"
124
+ tokenizer_config_path.write_text(
125
+ json.dumps(tokenizer_config, indent=2, ensure_ascii=False) + "\n",
126
+ encoding="utf-8",
127
+ )
128
+
129
 
130
  def load_precision_map(path: Path) -> dict:
131
  """Load the precision map JSON."""
 
200
  precision_map: dict,
201
  output_dir: Path,
202
  verbose: bool = True,
203
+ *,
204
+ source_revision: str | None = None,
205
  ) -> Path:
206
  """Apply mixed-precision quantization according to the precision map.
207
 
 
235
  return False
236
 
237
  if verbose:
238
+ print("\nPrecision map summary:")
239
  for pattern, precision in precision_map.items():
240
  if not pattern.startswith("_"):
241
  print(f" {pattern}: {precision}")
 
248
  convert(
249
  hf_path=model_path,
250
  mlx_path=str(staging_dir),
251
+ revision=source_revision,
252
  quantize=True,
253
  q_group_size=32,
254
  q_bits=8,
 
280
  normalize_unlimited_ocr_metadata(staging_dir)
281
 
282
  (staging_dir / "precision_map.json").write_text(
283
+ json.dumps(precision_map, indent=2, ensure_ascii=False) + "\n",
284
  encoding="utf-8",
285
  )
286
  (staging_dir / "quantization_summary.json").write_text(
 
291
  "source_model": (
292
  Path(model_path).name if Path(model_path).is_dir() else model_path
293
  ),
294
+ "source_revision": source_revision,
295
  "precision_map_sha256": _json_digest(precision_map),
296
  "quantized_module_count": len(set(quantized_modules)),
297
  "quantized_precision_counts": {
 
324
  )
325
  parser.add_argument("--model-path", required=True,
326
  help="Source model (baidu/Unlimited-OCR or local path)")
327
+ parser.add_argument(
328
+ "--source-revision",
329
+ default=None,
330
+ help="Immutable source commit used for remote loading and provenance",
331
+ )
332
  parser.add_argument("--precision-map", type=Path,
333
  default=Path(__file__).parent / "precision_map.json",
334
  help="Path to precision_map.json")
 
347
  print("=" * 60)
348
 
349
  precision_map = load_precision_map(args.precision_map)
350
+ convert_model(
351
+ args.model_path,
352
+ precision_map,
353
+ args.output_dir,
354
+ args.verbose,
355
+ source_revision=args.source_revision,
356
+ )
357
 
358
 
359
  if __name__ == "__main__":
quantization/release_gate.py CHANGED
@@ -7,40 +7,226 @@ import argparse
7
  import hashlib
8
  import importlib.metadata
9
  import json
 
10
  import platform
 
11
  from collections import Counter
12
  from datetime import datetime, timezone
13
- from pathlib import Path
14
  from typing import Any
15
 
16
 
 
 
 
 
 
17
  IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".bmp"}
18
  HARD_PROTECTED_GROUPS = {"vision_encoder", "lm_head"}
19
  EVIDENCE_PROTECTED_GROUPS = {"vision_projector", "token_embeddings"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  DEFAULT_THRESHOLDS = {
21
- "max_cer_delta_vs_bf16": 0.01,
 
22
  "max_cer_delta_vs_reference": 0.005,
23
  "max_digit_cer_delta_vs_bf16": 0.01,
24
  "max_table_score_degradation_vs_bf16": 0.01,
25
  "min_tps_ratio_vs_reference": 0.90,
26
  "max_weight_size_gb": 4.5,
 
 
 
 
27
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
 
30
  def load_json_object(path: str | Path) -> dict:
31
  """Load a JSON object and reject malformed release evidence."""
32
  path = Path(path)
33
  try:
34
- value = json.loads(path.read_text(encoding="utf-8"))
35
- except (OSError, UnicodeError, json.JSONDecodeError) as exc:
 
 
 
36
  raise ValueError(f"Invalid JSON file: {path}") from exc
37
  if not isinstance(value, dict):
38
  raise ValueError(f"JSON file must contain an object: {path}")
39
  return value
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def sha256_file(path: str | Path, chunk_size: int = 8 * 1024 * 1024) -> str:
43
  """Return the SHA-256 digest of a file without loading it into memory."""
 
 
 
 
 
 
44
  digest = hashlib.sha256()
45
  with Path(path).open("rb") as handle:
46
  while chunk := handle.read(chunk_size):
@@ -48,10 +234,91 @@ def sha256_file(path: str | Path, chunk_size: int = 8 * 1024 * 1024) -> str:
48
  return digest.hexdigest()
49
 
50
 
51
- def model_weight_manifest(model_dir: str | Path) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  """Hash Safetensors files and create a stable aggregate digest."""
 
 
53
  model_dir = Path(model_dir)
54
- weight_paths = sorted(path for path in model_dir.glob("*.safetensors") if path.is_file())
 
 
 
 
 
 
 
 
55
  if not weight_paths:
56
  raise FileNotFoundError(f"No Safetensors weights found in: {model_dir}")
57
 
@@ -78,37 +345,173 @@ def model_weight_manifest(model_dir: str | Path) -> dict:
78
  }
79
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def dataset_manifest(eval_dir: str | Path) -> dict:
82
- """Validate and content-hash an OCR image/ground-truth dataset."""
83
  eval_dir = Path(eval_dir)
84
  images_dir = eval_dir / "images"
85
  ground_truth_dir = eval_dir / "ground_truth"
86
  manifest_path = eval_dir / "manifest.json"
 
 
87
  if not images_dir.is_dir() or not ground_truth_dir.is_dir():
88
  raise FileNotFoundError("Evaluation dataset requires images/ and ground_truth/")
 
 
89
  declared = load_json_object(manifest_path)
90
 
91
- image_paths = sorted(
92
- path for path in images_dir.iterdir()
93
- if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS
94
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  if not image_paths:
96
  raise ValueError(f"Evaluation dataset contains no images: {images_dir}")
97
  duplicate_stems = sorted(
98
- stem for stem, count in Counter(path.stem for path in image_paths).items()
 
99
  if count > 1
100
  )
101
  if duplicate_stems:
102
  raise ValueError("Duplicate image stems: " + ", ".join(duplicate_stems))
103
 
104
- declared_samples = declared.get("samples", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  if not isinstance(declared_samples, list) or not declared_samples:
106
  raise ValueError("manifest.json must declare at least one sample")
107
- samples_by_image = {
108
- sample.get("image"): sample
109
- for sample in declared_samples
110
- if isinstance(sample, dict) and isinstance(sample.get("image"), str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  content_digest = hashlib.sha256()
114
  files = []
@@ -118,25 +521,43 @@ def dataset_manifest(eval_dir: str | Path) -> dict:
118
  table_samples = 0
119
  for image_path in image_paths:
120
  relative_image = image_path.relative_to(eval_dir).as_posix()
121
- sample = samples_by_image.get(relative_image)
122
- if sample is None:
123
- raise ValueError(f"Image is missing from manifest.json: {relative_image}")
124
- gt_path = ground_truth_dir / f"{image_path.stem}.txt"
125
- if not gt_path.is_file():
126
- raise FileNotFoundError(f"Missing ground truth for: {image_path.name}")
127
- ground_truth = gt_path.read_text(encoding="utf-8")
128
  if not ground_truth.strip():
129
  raise ValueError(f"Ground truth is empty: {gt_path}")
 
 
 
 
 
 
 
 
 
130
 
131
- category_counts[str(sample.get("category", "uncategorized"))] += 1
132
- digit_samples += int(any(character.isascii() and character.isdigit() for character in ground_truth))
133
- cjk_samples += int(any(
134
- "\u3040" <= character <= "\u30ff"
135
- or "\u3400" <= character <= "\u9fff"
136
- or "\uac00" <= character <= "\ud7af"
137
- for character in ground_truth
138
- ))
139
- table_samples += int("<table" in ground_truth.lower() or "| ---" in ground_truth)
 
 
 
 
 
 
 
 
 
140
 
141
  for path in (image_path, gt_path):
142
  relative = path.relative_to(eval_dir).as_posix()
@@ -148,12 +569,6 @@ def dataset_manifest(eval_dir: str | Path) -> dict:
148
  content_digest.update(b"\n")
149
  files.append({"path": relative, "size": size, "sha256": digest})
150
 
151
- declared_images = set(samples_by_image)
152
- actual_images = {path.relative_to(eval_dir).as_posix() for path in image_paths}
153
- extra = sorted(declared_images - actual_images)
154
- if extra:
155
- raise FileNotFoundError("Manifest references missing images: " + ", ".join(extra))
156
-
157
  content_digest.update(manifest_path.read_bytes())
158
  return {
159
  "name": eval_dir.name,
@@ -168,11 +583,69 @@ def dataset_manifest(eval_dir: str | Path) -> dict:
168
  }
169
 
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  def _pattern_is_within(child: str, parent: str) -> bool:
172
  """Return whether a precision rule is the parent itself or a descendant."""
173
  return child == parent or child.startswith(parent + ".")
174
 
175
 
 
 
 
 
 
 
 
 
 
 
176
  def generate_precision_map(
177
  base_map: dict,
178
  sensitivity_results: dict,
@@ -185,6 +658,13 @@ def generate_precision_map(
185
  """Turn group sensitivity measurements into executable top-level rules."""
186
  from quantization.layer_sensitivity import LAYER_GROUPS
187
 
 
 
 
 
 
 
 
188
  groups = sensitivity_results.get("layer_groups")
189
  if not isinstance(groups, dict) or not groups:
190
  raise ValueError("Sensitivity results contain no layer_groups")
@@ -206,30 +686,25 @@ def generate_precision_map(
206
  ("digit_cer_delta", digit_cer_threshold),
207
  ("table_score_degradation", table_degradation_threshold),
208
  ):
209
- value = result.get(key)
210
- if isinstance(value, (int, float)) and not isinstance(value, bool) and value > threshold:
 
 
211
  reasons.append(f"{key}={value:.6f}")
212
  measured_values = [
213
- result.get("cer_delta"),
214
- result.get("digit_cer_delta"),
215
- result.get("table_score_degradation"),
216
  ]
217
- explicit_non_degradation = (
218
- result.get("status") == "success"
219
- and all(
220
- value is None
221
- or (
222
- isinstance(value, (int, float))
223
- and not isinstance(value, bool)
224
- and value <= 0
225
- )
226
- for value in measured_values
227
- )
228
  )
229
  if group_name in HARD_PROTECTED_GROUPS:
230
  reasons.append("protected OCR-critical group")
231
  elif group_name in EVIDENCE_PROTECTED_GROUPS and not explicit_non_degradation:
232
- reasons.append("protected unless all measured quality deltas are non-degrading")
 
 
233
 
234
  retain_bf16 = bool(reasons)
235
  if retain_bf16:
@@ -246,11 +721,14 @@ def generate_precision_map(
246
  generated[group_pattern] = "mxfp8"
247
  decisions[group_name] = {
248
  "precision": (
249
- "bfloat16" if retain_bf16
250
- else "mxfp8" if group_name in EVIDENCE_PROTECTED_GROUPS
 
 
251
  else "base-map"
252
  ),
253
- "reasons": reasons or (
 
254
  ["all measured quality deltas were non-degrading"]
255
  if group_name in EVIDENCE_PROTECTED_GROUPS
256
  else ["within thresholds"]
@@ -262,10 +740,38 @@ def generate_precision_map(
262
  overrides = calibration_results.get("precision_overrides")
263
  if not isinstance(overrides, dict) or not overrides:
264
  raise ValueError("Calibration results contain no precision_overrides")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  allowed_precisions = {"bfloat16", "mxfp8", "affine8"}
266
  for pattern, precision in overrides.items():
267
  if not isinstance(pattern, str) or precision not in allowed_precisions:
268
- raise ValueError(f"Invalid calibrated precision override: {pattern}={precision}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  generated[pattern] = precision
270
  for group_name, patterns in LAYER_GROUPS.items():
271
  if pattern in patterns:
@@ -299,30 +805,548 @@ def validate_candidate_metadata(model_dir: str | Path) -> dict:
299
  model_dir = Path(model_dir)
300
  config = load_json_object(model_dir / "config.json")
301
  processor = load_json_object(model_dir / "processor_config.json")
 
 
302
  architectures = config.get("architectures")
303
  quantization = config.get("quantization") or config.get("quantization_config")
304
  text_config = config.get("text_config") or config.get("language_config") or config
305
- window = text_config.get("sliding_window_size") if isinstance(text_config, dict) else None
 
 
 
 
306
  if window is None and isinstance(text_config, dict):
307
  window = text_config.get("sliding_window")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  checks = {
309
- "architecture": isinstance(architectures, list) and "UnlimitedOCRForCausalLM" in architectures,
 
310
  "model_type": config.get("model_type") == "unlimited-ocr",
311
  "mxfp8": isinstance(quantization, dict) and quantization.get("mode") == "mxfp8",
312
- "sliding_window": isinstance(window, int) and not isinstance(window, bool) and window > 0,
313
- "processor_class": processor.get("processor_class") == "UnlimitedOCRHFProcessor",
 
 
 
 
314
  "sft_format": processor.get("sft_format") == "unlimitedocr",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  }
316
- return {"passed": all(checks.values()), "checks": checks, "sliding_window": window}
317
 
318
 
319
  def _number(payload: dict, key: str) -> float | None:
320
- value = payload.get(key)
321
- return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
 
324
  def _gate(name: str, actual: Any, limit: Any, passed: bool, detail: str) -> dict:
325
- return {"name": name, "passed": bool(passed), "actual": actual, "limit": limit, "detail": detail}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
 
328
  def evaluate_release_gates(
@@ -330,6 +1354,7 @@ def evaluate_release_gates(
330
  candidate_weights: dict,
331
  reference_weights: dict,
332
  dataset: dict,
 
333
  metadata: dict,
334
  bf16_accuracy: dict,
335
  reference_accuracy: dict,
@@ -337,28 +1362,72 @@ def evaluate_release_gates(
337
  reference_performance: dict,
338
  candidate_performance: dict,
339
  rswa: dict,
 
 
 
 
 
 
 
 
 
340
  thresholds: dict | None = None,
341
  ) -> list[dict]:
342
  """Evaluate every required release gate; missing values fail closed."""
343
- limits = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
344
  gates = []
345
  candidate_digest = candidate_weights.get("aggregate_sha256")
346
  reference_digest = reference_weights.get("aggregate_sha256")
347
- gates.append(_gate(
348
- "weights_are_distinct", candidate_digest, f"different from {reference_digest}",
349
- bool(candidate_digest and reference_digest and candidate_digest != reference_digest),
350
- "Candidate aggregate digest must differ from the Sahil reference",
351
- ))
 
 
 
 
 
 
 
 
352
  candidate_size = _number(candidate_weights, "total_size_gb")
353
- gates.append(_gate(
354
- "weight_size_gb", candidate_size, limits["max_weight_size_gb"],
355
- candidate_size is not None and candidate_size <= limits["max_weight_size_gb"],
356
- "Candidate Safetensors size",
357
- ))
358
- gates.append(_gate(
359
- "native_model_metadata", metadata.get("checks"), True,
360
- metadata.get("passed") is True, "Native Unlimited-OCR, MXFP8, and R-SWA metadata",
361
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
  coverage = {
364
  "samples": dataset.get("num_samples", 0),
@@ -366,77 +1435,388 @@ def evaluate_release_gates(
366
  "cjk": dataset.get("num_cjk_samples", 0),
367
  "table": dataset.get("num_table_samples", 0),
368
  }
369
- gates.append(_gate(
370
- "evaluation_coverage", coverage, "all counts > 0",
371
- all(isinstance(value, int) and value > 0 for value in coverage.values()),
372
- "Dataset must cover ordinary text, digits, CJK, and tables",
373
- ))
374
- sample_counts = [
375
- bf16_accuracy.get("num_samples"),
376
- reference_accuracy.get("num_samples"),
377
- candidate_accuracy.get("num_samples"),
378
- ]
379
- gates.append(_gate(
380
- "same_evaluation_samples", sample_counts, dataset.get("num_samples"),
381
- all(count == dataset.get("num_samples") for count in sample_counts),
382
- "All three checkpoints must run the complete evaluation set",
383
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
  bf16_cer = _number(bf16_accuracy, "mean_cer")
386
  reference_cer = _number(reference_accuracy, "mean_cer")
387
  candidate_cer = _number(candidate_accuracy, "mean_cer")
388
- delta_bf16 = candidate_cer - bf16_cer if candidate_cer is not None and bf16_cer is not None else None
389
- delta_reference = candidate_cer - reference_cer if candidate_cer is not None and reference_cer is not None else None
390
- gates.append(_gate(
391
- "candidate_cer_vs_bf16", delta_bf16, limits["max_cer_delta_vs_bf16"],
392
- delta_bf16 is not None and delta_bf16 <= limits["max_cer_delta_vs_bf16"],
393
- "Candidate minus BF16 absolute mean CER",
394
- ))
395
- gates.append(_gate(
396
- "candidate_cer_vs_reference", delta_reference, limits["max_cer_delta_vs_reference"],
397
- delta_reference is not None and delta_reference <= limits["max_cer_delta_vs_reference"],
398
- "Candidate minus Sahil-reference absolute mean CER",
399
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
402
  candidate_digit = _number(candidate_accuracy, "mean_digit_cer")
403
- digit_delta = candidate_digit - bf16_digit if candidate_digit is not None and bf16_digit is not None else None
404
- gates.append(_gate(
405
- "candidate_digit_cer_vs_bf16", digit_delta, limits["max_digit_cer_delta_vs_bf16"],
406
- digit_delta is not None and digit_delta <= limits["max_digit_cer_delta_vs_bf16"],
407
- "Candidate minus BF16 digit CER",
408
- ))
 
 
 
 
 
 
 
 
 
409
 
410
  bf16_table = _number(bf16_accuracy, "mean_table_score")
411
  candidate_table = _number(candidate_accuracy, "mean_table_score")
412
- table_degradation = bf16_table - candidate_table if bf16_table is not None and candidate_table is not None else None
413
- gates.append(_gate(
414
- "candidate_table_score_vs_bf16", table_degradation,
415
- limits["max_table_score_degradation_vs_bf16"],
416
- table_degradation is not None and table_degradation <= limits["max_table_score_degradation_vs_bf16"],
417
- "BF16 minus candidate mean table score",
418
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
  reference_tps = _number(reference_performance, "mean_tps")
421
  candidate_tps = _number(candidate_performance, "mean_tps")
422
- tps_ratio = candidate_tps / reference_tps if candidate_tps is not None and reference_tps and reference_tps > 0 else None
423
- gates.append(_gate(
424
- "candidate_tps_vs_reference", tps_ratio, limits["min_tps_ratio_vs_reference"],
425
- tps_ratio is not None and tps_ratio >= limits["min_tps_ratio_vs_reference"],
426
- "Candidate decode throughput divided by Sahil-reference throughput",
427
- ))
428
- gates.append(_gate(
429
- "rswa_8k_bounded", rswa.get("pass_conditions"), True,
430
- rswa.get("passed") is True, "8K generation, bounded cache, and stable throughput",
431
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  return gates
433
 
434
 
435
  def _metric_summary(payload: dict) -> dict:
436
  keys = (
437
- "model_path", "num_samples", "mean_cer", "mean_digit_cer",
438
- "mean_cjk_cer", "mean_table_score", "mean_tps",
439
- "mean_peak_memory_mb", "max_tokens", "profile",
 
 
 
 
 
 
 
440
  )
441
  return {key: payload.get(key) for key in keys if key in payload}
442
 
@@ -456,33 +1836,64 @@ def build_release_manifest(
456
  candidate_dir: str | Path,
457
  reference_dir: str | Path,
458
  source_dir: str | Path,
 
459
  eval_dir: str | Path,
460
  evidence_paths: dict[str, str | Path],
461
  repo_id: str,
462
  source_id: str,
 
463
  reference_id: str,
 
464
  thresholds: dict | None = None,
465
  ) -> dict:
466
  """Build a complete release decision from on-disk evidence."""
467
- evidence = {name: load_json_object(path) for name, path in evidence_paths.items()}
468
- required = {
469
- "bf16_accuracy", "reference_accuracy", "candidate_accuracy",
470
- "reference_performance", "candidate_performance", "candidate_rswa",
471
- "sensitivity_results", "calibration_results", "generated_precision_map", "provenance",
472
- }
473
- missing = sorted(required - set(evidence))
474
  if missing:
475
  raise ValueError("Missing release evidence: " + ", ".join(missing))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
  candidate_weights = model_weight_manifest(candidate_dir)
478
- reference_weights = model_weight_manifest(reference_dir)
 
 
 
479
  source_weights = model_weight_manifest(source_dir)
 
480
  dataset = dataset_manifest(eval_dir)
481
  metadata = validate_candidate_metadata(candidate_dir)
 
 
 
 
 
 
 
 
 
 
 
 
482
  gates = evaluate_release_gates(
483
  candidate_weights=candidate_weights,
484
  reference_weights=reference_weights,
485
  dataset=dataset,
 
486
  metadata=metadata,
487
  bf16_accuracy=evidence["bf16_accuracy"],
488
  reference_accuracy=evidence["reference_accuracy"],
@@ -490,8 +1901,126 @@ def build_release_manifest(
490
  reference_performance=evidence["reference_performance"],
491
  candidate_performance=evidence["candidate_performance"],
492
  rswa=evidence["candidate_rswa"],
493
- thresholds=thresholds,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  artifact_hashes = {
496
  name: {
497
  "filename": Path(path).name,
@@ -500,26 +2029,39 @@ def build_release_manifest(
500
  }
501
  for name, path in evidence_paths.items()
502
  }
503
- limits = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
504
  return {
505
- "schema_version": 1,
506
  "created_at": datetime.now(timezone.utc).isoformat(),
507
  "release_approved": all(gate["passed"] for gate in gates),
508
  "repo_id": repo_id,
509
- "source": {"id": source_id, "weights": source_weights},
510
- "reference": {"id": reference_id, "weights": reference_weights},
 
 
 
 
 
 
 
 
511
  "candidate": {
512
  "name": Path(candidate_dir).name,
513
  "weights": candidate_weights,
 
514
  "metadata": metadata,
515
  },
516
- "dataset": dataset,
 
 
517
  "thresholds": limits,
518
  "metrics": {
519
  name: _metric_summary(evidence[name])
520
  for name in (
521
- "bf16_accuracy", "reference_accuracy", "candidate_accuracy",
522
- "reference_performance", "candidate_performance",
 
 
 
523
  )
524
  },
525
  "rswa": evidence["candidate_rswa"],
@@ -545,36 +2087,48 @@ def main() -> None:
545
  parser.add_argument("--candidate-dir", required=True, type=Path)
546
  parser.add_argument("--reference-dir", required=True, type=Path)
547
  parser.add_argument("--source-dir", required=True, type=Path)
 
548
  parser.add_argument("--eval-dir", required=True, type=Path)
549
  parser.add_argument("--artifacts-dir", required=True, type=Path)
550
  parser.add_argument("--output", required=True, type=Path)
551
  parser.add_argument("--repo-id", required=True)
552
  parser.add_argument("--source-id", default="baidu/Unlimited-OCR")
553
- parser.add_argument("--reference-id", default="sahilchachra/unlimited-ocr-mxfp8-mlx")
 
 
 
 
554
  args = parser.parse_args()
555
 
556
  evidence_paths = {
557
  name: args.artifacts_dir / f"{name}.json"
558
- for name in (
559
- "bf16_accuracy", "reference_accuracy", "candidate_accuracy",
560
- "reference_performance", "candidate_performance", "candidate_rswa",
561
- "sensitivity_results", "calibration_results", "generated_precision_map", "provenance",
562
- )
563
  }
 
 
 
 
564
  manifest = build_release_manifest(
565
  candidate_dir=args.candidate_dir,
566
  reference_dir=args.reference_dir,
567
  source_dir=args.source_dir,
 
568
  eval_dir=args.eval_dir,
569
  evidence_paths=evidence_paths,
570
  repo_id=args.repo_id,
571
  source_id=args.source_id,
 
572
  reference_id=args.reference_id,
 
573
  )
574
  args.output.parent.mkdir(parents=True, exist_ok=True)
575
- args.output.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
 
 
576
  for gate in manifest["gates"]:
577
- print(f"[{'PASS' if gate['passed'] else 'FAIL'}] {gate['name']}: {gate['actual']}")
 
 
578
  print(f"Release approved: {manifest['release_approved']}")
579
  if not manifest["release_approved"]:
580
  raise SystemExit(1)
 
7
  import hashlib
8
  import importlib.metadata
9
  import json
10
+ import math
11
  import platform
12
+ import re
13
  from collections import Counter
14
  from datetime import datetime, timezone
15
+ from pathlib import Path, PurePosixPath
16
  from typing import Any
17
 
18
 
19
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
20
+ import sys
21
+
22
+ if str(PROJECT_ROOT) not in sys.path:
23
+ sys.path.insert(0, str(PROJECT_ROOT))
24
  IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".bmp"}
25
  HARD_PROTECTED_GROUPS = {"vision_encoder", "lm_head"}
26
  EVIDENCE_PROTECTED_GROUPS = {"vision_projector", "token_embeddings"}
27
+ CANDIDATE_METADATA_FILES = (
28
+ "config.json",
29
+ "model.safetensors.index.json",
30
+ "processor_config.json",
31
+ "special_tokens_map.json",
32
+ "tokenizer.json",
33
+ "tokenizer_config.json",
34
+ "chat_template.jinja",
35
+ "precision_map.json",
36
+ "quantization_summary.json",
37
+ )
38
+ REQUIRED_CANDIDATE_METADATA_FILES = {
39
+ "config.json",
40
+ "processor_config.json",
41
+ "tokenizer.json",
42
+ "precision_map.json",
43
+ "quantization_summary.json",
44
+ }
45
+ RELEASE_PROJECT_FILES = (
46
+ "LICENSE",
47
+ "pyproject.toml",
48
+ "generation_config.json",
49
+ "preprocessing_config.json",
50
+ "quantization_config.json",
51
+ "docs/PRD.md",
52
+ "docs/TECHNICAL_SPEC.md",
53
+ "docs/adr/0001-verified-ocr-aware-quantization.md",
54
+ "docs/adr/0002-calibrated-affine8-lm-head.md",
55
+ "src/unlimited_ocr/__init__.py",
56
+ "src/unlimited_ocr/engine.py",
57
+ "src/unlimited_ocr/pipeline.py",
58
+ "src/unlimited_ocr/preprocessing.py",
59
+ "src/unlimited_ocr/output.py",
60
+ "src/unlimited_ocr/pdf.py",
61
+ "src/unlimited_ocr/cli.py",
62
+ "src/unlimited_ocr/profiles.py",
63
+ "src/unlimited_ocr/server.py",
64
+ "src/unlimited_ocr/adapter_registry.json",
65
+ "quantization/layer_sensitivity.py",
66
+ "quantization/calibrate_precision.py",
67
+ "quantization/mixed_precision_convert.py",
68
+ "quantization/precision_map.json",
69
+ "quantization/release_gate.py",
70
+ "quantization/run_pipeline.py",
71
+ "benchmarks/evaluate_cer.py",
72
+ "benchmarks/evaluate_tables.py",
73
+ "benchmarks/normalize_output.py",
74
+ "benchmarks/run_accuracy.py",
75
+ "benchmarks/run_performance.py",
76
+ "benchmarks/rswa_validation.py",
77
+ "benchmarks/datasets.md",
78
+ "examples/single_image.py",
79
+ "examples/multi_page_pdf.py",
80
+ "examples/batch_directory.py",
81
+ )
82
+ CALIBRATION_EVIDENCE_KEYS = {
83
+ "bfloat16": (
84
+ "calibration_bfloat16_accuracy",
85
+ "calibration_bfloat16_performance",
86
+ ),
87
+ "mxfp8": (
88
+ "calibration_mxfp8_accuracy",
89
+ "calibration_mxfp8_performance",
90
+ ),
91
+ "affine8": (
92
+ "calibration_affine8_accuracy",
93
+ "calibration_affine8_performance",
94
+ ),
95
+ }
96
+ CALIBRATION_BASELINE_EVIDENCE_KEYS = {
97
+ "calibration_baseline_accuracy",
98
+ "calibration_reference_performance",
99
+ }
100
+ CALIBRATION_RAW_EVIDENCE_NAMES = frozenset(
101
+ name for pair in CALIBRATION_EVIDENCE_KEYS.values() for name in pair
102
+ ) | frozenset(CALIBRATION_BASELINE_EVIDENCE_KEYS)
103
+ RELEASE_EVIDENCE_NAMES = (
104
+ frozenset(
105
+ {
106
+ "bf16_accuracy",
107
+ "reference_accuracy",
108
+ "candidate_accuracy",
109
+ "reference_performance",
110
+ "candidate_performance",
111
+ "candidate_rswa",
112
+ "sensitivity_results",
113
+ "calibration_results",
114
+ "generated_precision_map",
115
+ "provenance",
116
+ }
117
+ )
118
+ | CALIBRATION_RAW_EVIDENCE_NAMES
119
+ )
120
  DEFAULT_THRESHOLDS = {
121
+ # n≈12 held-out macro CER is noisy; allow 1.5pp absolute vs BF16.
122
+ "max_cer_delta_vs_bf16": 0.015,
123
  "max_cer_delta_vs_reference": 0.005,
124
  "max_digit_cer_delta_vs_bf16": 0.01,
125
  "max_table_score_degradation_vs_bf16": 0.01,
126
  "min_tps_ratio_vs_reference": 0.90,
127
  "max_weight_size_gb": 4.5,
128
+ "min_rswa_tokens": 8192,
129
+ # Forced-min-token R-SWA runs suppress EOS through 8k; some residual
130
+ # loopiness is expected. Cap still catches fully degenerate collapse.
131
+ "max_rswa_repetition_rate": 0.25,
132
  }
133
+ MLX_RELEASE_SCHEMA_VERSION = 3
134
+ MLX_RELEASE_GATE_NAMES = (
135
+ "weights_are_distinct",
136
+ "weight_size_gb",
137
+ "native_model_metadata",
138
+ "candidate_source_provenance",
139
+ "evaluation_coverage",
140
+ "held_out_evaluation_dataset",
141
+ "accuracy_aggregates_recomputed",
142
+ "same_evaluation_samples",
143
+ "model_identities",
144
+ "immutable_model_revisions",
145
+ "same_accuracy_recipe",
146
+ "candidate_cer_vs_bf16",
147
+ "candidate_cer_vs_reference",
148
+ "candidate_digit_cer_vs_bf16",
149
+ "candidate_table_score_vs_bf16",
150
+ "performance_aggregates_recomputed",
151
+ "same_performance_setup",
152
+ "candidate_tps_vs_reference",
153
+ "rswa_8k_bounded",
154
+ "provenance_matches_release",
155
+ "calibration_recomputed",
156
+ "sensitivity_matches_calibration_dataset",
157
+ "candidate_precision_map_matches_evidence",
158
+ "precision_map_reproducible",
159
+ )
160
+
161
+
162
+ def _reject_json_constant(value: str) -> None:
163
+ raise ValueError(f"non-finite JSON number: {value}")
164
+
165
+
166
+ def _json_digest(value: Any) -> str:
167
+ payload = json.dumps(
168
+ value,
169
+ sort_keys=True,
170
+ separators=(",", ":"),
171
+ ensure_ascii=False,
172
+ allow_nan=False,
173
+ ).encode("utf-8")
174
+ return hashlib.sha256(payload).hexdigest()
175
 
176
 
177
  def load_json_object(path: str | Path) -> dict:
178
  """Load a JSON object and reject malformed release evidence."""
179
  path = Path(path)
180
  try:
181
+ value = json.loads(
182
+ path.read_text(encoding="utf-8"),
183
+ parse_constant=_reject_json_constant,
184
+ )
185
+ except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
186
  raise ValueError(f"Invalid JSON file: {path}") from exc
187
  if not isinstance(value, dict):
188
  raise ValueError(f"JSON file must contain an object: {path}")
189
  return value
190
 
191
 
192
+ def calibration_raw_evidence_paths(
193
+ artifacts_dir: str | Path,
194
+ calibration: dict,
195
+ ) -> dict[str, Path]:
196
+ """Resolve the eight allowlisted raw calibration artifacts safely."""
197
+ artifacts_dir = Path(artifacts_dir)
198
+ if artifacts_dir.is_symlink() or not artifacts_dir.is_dir():
199
+ raise FileNotFoundError(
200
+ f"Calibration artifacts directory not found: {artifacts_dir}"
201
+ )
202
+ calibration_inputs = calibration.get("input_artifacts")
203
+ if not isinstance(calibration_inputs, dict):
204
+ raise ValueError("Calibration input artifact records are missing")
205
+ paths = {}
206
+ filenames = set()
207
+ for name in CALIBRATION_RAW_EVIDENCE_NAMES:
208
+ record = calibration_inputs.get(name)
209
+ filename = record.get("filename") if isinstance(record, dict) else None
210
+ if (
211
+ not isinstance(filename, str)
212
+ or not filename
213
+ or Path(filename).name != filename
214
+ or filename in filenames
215
+ ):
216
+ raise ValueError(f"Calibration input filename is invalid: {name}")
217
+ filenames.add(filename)
218
+ paths[name] = artifacts_dir / filename
219
+ return paths
220
+
221
+
222
  def sha256_file(path: str | Path, chunk_size: int = 8 * 1024 * 1024) -> str:
223
  """Return the SHA-256 digest of a file without loading it into memory."""
224
+ if (
225
+ not isinstance(chunk_size, int)
226
+ or isinstance(chunk_size, bool)
227
+ or chunk_size < 1
228
+ ):
229
+ raise ValueError("chunk_size must be a positive integer")
230
  digest = hashlib.sha256()
231
  with Path(path).open("rb") as handle:
232
  while chunk := handle.read(chunk_size):
 
234
  return digest.hexdigest()
235
 
236
 
237
+ def release_files_manifest(
238
+ base_dir: str | Path,
239
+ relative_paths: tuple[str, ...],
240
+ *,
241
+ required: bool,
242
+ ) -> dict[str, Any]:
243
+ """Hash an allowlisted set of regular files for publication approval."""
244
+ base_dir = Path(base_dir)
245
+ if base_dir.is_symlink():
246
+ raise ValueError(f"Release directory must not be a symbolic link: {base_dir}")
247
+ if len(set(relative_paths)) != len(relative_paths):
248
+ raise ValueError("Release file allowlist contains duplicates")
249
+ records: list[dict[str, Any]] = []
250
+ aggregate = hashlib.sha256()
251
+ missing: list[str] = []
252
+ for relative in relative_paths:
253
+ pure_path = PurePosixPath(relative)
254
+ if (
255
+ not relative
256
+ or pure_path.is_absolute()
257
+ or pure_path.as_posix() != relative
258
+ or any(part in {"", ".", ".."} for part in pure_path.parts)
259
+ ):
260
+ raise ValueError(f"Unsafe release file path: {relative!r}")
261
+ path = base_dir.joinpath(*pure_path.parts)
262
+ parents = [
263
+ base_dir.joinpath(*pure_path.parts[:index])
264
+ for index in range(1, len(pure_path.parts) + 1)
265
+ ]
266
+ if any(parent.is_symlink() for parent in parents):
267
+ raise ValueError(f"Release files must not be symbolic links: {relative}")
268
+ if not path.is_file():
269
+ if required:
270
+ missing.append(relative)
271
+ continue
272
+ size = path.stat().st_size
273
+ digest = sha256_file(path)
274
+ aggregate.update(relative.encode("utf-8"))
275
+ aggregate.update(b"\0")
276
+ aggregate.update(str(size).encode("ascii"))
277
+ aggregate.update(b"\0")
278
+ aggregate.update(digest.encode("ascii"))
279
+ aggregate.update(b"\n")
280
+ records.append({"path": relative, "size": size, "sha256": digest})
281
+ if missing:
282
+ raise FileNotFoundError(
283
+ "Required release files are missing: " + ", ".join(missing)
284
+ )
285
+ return {"files": records, "aggregate_sha256": aggregate.hexdigest()}
286
+
287
+
288
+ def candidate_metadata_manifest(model_dir: str | Path) -> dict[str, Any]:
289
+ """Hash every candidate metadata file the publisher may upload."""
290
+ manifest = release_files_manifest(
291
+ model_dir,
292
+ CANDIDATE_METADATA_FILES,
293
+ required=False,
294
+ )
295
+ present = {record["path"] for record in manifest["files"]}
296
+ missing = sorted(REQUIRED_CANDIDATE_METADATA_FILES - present)
297
+ if missing:
298
+ raise FileNotFoundError(
299
+ "Candidate is missing required files: " + ", ".join(missing)
300
+ )
301
+ return manifest
302
+
303
+
304
+ def model_weight_manifest(
305
+ model_dir: str | Path,
306
+ *,
307
+ allow_symlinks: bool = False,
308
+ ) -> dict:
309
  """Hash Safetensors files and create a stable aggregate digest."""
310
+ if not isinstance(allow_symlinks, bool):
311
+ raise TypeError("allow_symlinks must be a boolean")
312
  model_dir = Path(model_dir)
313
+ if model_dir.is_symlink():
314
+ raise ValueError("Model directory must not be a symbolic link")
315
+ weight_candidates = sorted(model_dir.glob("*.safetensors"))
316
+ symlinks = [path.name for path in weight_candidates if path.is_symlink()]
317
+ if symlinks and not allow_symlinks:
318
+ raise ValueError(
319
+ "Safetensors weights must not be symbolic links: " + ", ".join(symlinks)
320
+ )
321
+ weight_paths = [path for path in weight_candidates if path.is_file()]
322
  if not weight_paths:
323
  raise FileNotFoundError(f"No Safetensors weights found in: {model_dir}")
324
 
 
345
  }
346
 
347
 
348
+ def _declared_dataset_path(
349
+ value: Any,
350
+ *,
351
+ directory: str,
352
+ allowed_suffixes: set[str],
353
+ ) -> str:
354
+ """Validate a normalized, direct child path in a dataset manifest."""
355
+ if (
356
+ not isinstance(value, str)
357
+ or not value
358
+ or value.strip() != value
359
+ or "\\" in value
360
+ ):
361
+ raise ValueError(f"manifest sample requires a valid {directory} path")
362
+ path = PurePosixPath(value)
363
+ if (
364
+ path.is_absolute()
365
+ or len(path.parts) != 2
366
+ or path.parts[0] != directory
367
+ or path.as_posix() != value
368
+ or path.suffix.lower() not in allowed_suffixes
369
+ ):
370
+ raise ValueError(f"Unsafe or unsupported dataset path: {value}")
371
+ return value
372
+
373
+
374
  def dataset_manifest(eval_dir: str | Path) -> dict:
375
+ """Validate and content-hash an exact OCR image/ground-truth dataset."""
376
  eval_dir = Path(eval_dir)
377
  images_dir = eval_dir / "images"
378
  ground_truth_dir = eval_dir / "ground_truth"
379
  manifest_path = eval_dir / "manifest.json"
380
+ if eval_dir.is_symlink() or images_dir.is_symlink() or ground_truth_dir.is_symlink():
381
+ raise ValueError("Evaluation dataset directories must not be symbolic links")
382
  if not images_dir.is_dir() or not ground_truth_dir.is_dir():
383
  raise FileNotFoundError("Evaluation dataset requires images/ and ground_truth/")
384
+ if manifest_path.is_symlink():
385
+ raise ValueError("Evaluation manifest must not be a symbolic link")
386
  declared = load_json_object(manifest_path)
387
 
388
+ image_entries = sorted(images_dir.iterdir())
389
+ invalid_image_entries = [
390
+ path.name
391
+ for path in image_entries
392
+ if path.is_symlink()
393
+ or not path.is_file()
394
+ or path.suffix.lower() not in IMAGE_EXTENSIONS
395
+ ]
396
+ if invalid_image_entries:
397
+ raise ValueError(
398
+ "Evaluation images directory contains unsupported entries: "
399
+ + ", ".join(invalid_image_entries)
400
+ )
401
+ image_candidates = image_entries
402
+ image_symlinks = [path.name for path in image_candidates if path.is_symlink()]
403
+ if image_symlinks:
404
+ raise ValueError(
405
+ "Evaluation images must not be symbolic links: " + ", ".join(image_symlinks)
406
+ )
407
+ image_paths = [path for path in image_candidates if path.is_file()]
408
  if not image_paths:
409
  raise ValueError(f"Evaluation dataset contains no images: {images_dir}")
410
  duplicate_stems = sorted(
411
+ stem
412
+ for stem, count in Counter(path.stem for path in image_paths).items()
413
  if count > 1
414
  )
415
  if duplicate_stems:
416
  raise ValueError("Duplicate image stems: " + ", ".join(duplicate_stems))
417
 
418
+ ground_truth_entries = sorted(ground_truth_dir.iterdir())
419
+ invalid_ground_truth_entries = [
420
+ path.name
421
+ for path in ground_truth_entries
422
+ if path.is_symlink() or not path.is_file() or path.suffix.lower() != ".txt"
423
+ ]
424
+ if invalid_ground_truth_entries:
425
+ raise ValueError(
426
+ "Ground-truth directory contains unsupported entries: "
427
+ + ", ".join(invalid_ground_truth_entries)
428
+ )
429
+ ground_truth_candidates = ground_truth_entries
430
+ ground_truth_symlinks = [
431
+ path.name for path in ground_truth_candidates if path.is_symlink()
432
+ ]
433
+ if ground_truth_symlinks:
434
+ raise ValueError(
435
+ "Ground-truth files must not be symbolic links: "
436
+ + ", ".join(ground_truth_symlinks)
437
+ )
438
+ ground_truth_paths = [path for path in ground_truth_candidates if path.is_file()]
439
+
440
+ declared_samples = declared.get("samples")
441
  if not isinstance(declared_samples, list) or not declared_samples:
442
  raise ValueError("manifest.json must declare at least one sample")
443
+ samples_by_image: dict[str, dict[str, Any]] = {}
444
+ declared_ground_truth: set[str] = set()
445
+ declared_ids: set[int] = set()
446
+ for index, sample in enumerate(declared_samples, start=1):
447
+ if not isinstance(sample, dict):
448
+ raise ValueError(f"Manifest sample {index} must be an object")
449
+ relative_image = _declared_dataset_path(
450
+ sample.get("image"),
451
+ directory="images",
452
+ allowed_suffixes=IMAGE_EXTENSIONS,
453
+ )
454
+ relative_ground_truth = _declared_dataset_path(
455
+ sample.get("ground_truth"),
456
+ directory="ground_truth",
457
+ allowed_suffixes={".txt"},
458
+ )
459
+ expected_ground_truth = f"ground_truth/{PurePosixPath(relative_image).stem}.txt"
460
+ if relative_ground_truth != expected_ground_truth:
461
+ raise ValueError(
462
+ f"Ground truth must match the image stem: {relative_image}"
463
+ )
464
+ category = sample.get("category")
465
+ sample_id = sample.get("id")
466
+ if (
467
+ not isinstance(sample_id, int)
468
+ or isinstance(sample_id, bool)
469
+ or sample_id < 1
470
+ or sample_id in declared_ids
471
+ ):
472
+ raise ValueError(f"Manifest sample {index} requires a unique positive ID")
473
+ declared_ids.add(sample_id)
474
+ if (
475
+ not isinstance(category, str)
476
+ or not category.strip()
477
+ or category != category.strip()
478
+ ):
479
+ raise ValueError(f"Manifest sample {index} requires a category")
480
+ if relative_image in samples_by_image:
481
+ raise ValueError(f"Duplicate declared image: {relative_image}")
482
+ if relative_ground_truth in declared_ground_truth:
483
+ raise ValueError(
484
+ f"Duplicate declared ground truth: {relative_ground_truth}"
485
+ )
486
+ samples_by_image[relative_image] = sample
487
+ declared_ground_truth.add(relative_ground_truth)
488
+
489
+ actual_images = {path.relative_to(eval_dir).as_posix() for path in image_paths}
490
+ actual_ground_truth = {
491
+ path.relative_to(eval_dir).as_posix() for path in ground_truth_paths
492
  }
493
+ missing_images = sorted(set(samples_by_image) - actual_images)
494
+ undeclared_images = sorted(actual_images - set(samples_by_image))
495
+ missing_ground_truth = sorted(declared_ground_truth - actual_ground_truth)
496
+ undeclared_ground_truth = sorted(actual_ground_truth - declared_ground_truth)
497
+ if missing_images:
498
+ raise FileNotFoundError(
499
+ "Manifest references missing images: " + ", ".join(missing_images)
500
+ )
501
+ if undeclared_images:
502
+ raise ValueError(
503
+ "Images are missing from manifest.json: " + ", ".join(undeclared_images)
504
+ )
505
+ if missing_ground_truth:
506
+ raise FileNotFoundError(
507
+ "Manifest references missing ground truth: "
508
+ + ", ".join(missing_ground_truth)
509
+ )
510
+ if undeclared_ground_truth:
511
+ raise ValueError(
512
+ "Ground-truth files are missing from manifest.json: "
513
+ + ", ".join(undeclared_ground_truth)
514
+ )
515
 
516
  content_digest = hashlib.sha256()
517
  files = []
 
521
  table_samples = 0
522
  for image_path in image_paths:
523
  relative_image = image_path.relative_to(eval_dir).as_posix()
524
+ sample = samples_by_image[relative_image]
525
+ relative_ground_truth = str(sample["ground_truth"])
526
+ gt_path = eval_dir / relative_ground_truth
527
+ try:
528
+ ground_truth = gt_path.read_text(encoding="utf-8")
529
+ except (OSError, UnicodeError) as exc:
530
+ raise ValueError(f"Ground truth is not valid UTF-8: {gt_path}") from exc
531
  if not ground_truth.strip():
532
  raise ValueError(f"Ground truth is empty: {gt_path}")
533
+ try:
534
+ from PIL import Image
535
+
536
+ with Image.open(image_path) as image:
537
+ image.verify()
538
+ if image.width < 1 or image.height < 1:
539
+ raise ValueError("image has no pixels")
540
+ except (OSError, ValueError) as exc:
541
+ raise ValueError(f"Evaluation image is unreadable: {image_path}") from exc
542
 
543
+ category_counts[str(sample["category"])] += 1
544
+ digit_samples += int(
545
+ any(
546
+ character.isascii() and character.isdigit()
547
+ for character in ground_truth
548
+ )
549
+ )
550
+ cjk_samples += int(
551
+ any(
552
+ "\u3040" <= character <= "\u30ff"
553
+ or "\u3400" <= character <= "\u9fff"
554
+ or "\uac00" <= character <= "\ud7af"
555
+ for character in ground_truth
556
+ )
557
+ )
558
+ table_samples += int(
559
+ "<table" in ground_truth.lower() or "| ---" in ground_truth
560
+ )
561
 
562
  for path in (image_path, gt_path):
563
  relative = path.relative_to(eval_dir).as_posix()
 
569
  content_digest.update(b"\n")
570
  files.append({"path": relative, "size": size, "sha256": digest})
571
 
 
 
 
 
 
 
572
  content_digest.update(manifest_path.read_bytes())
573
  return {
574
  "name": eval_dir.name,
 
583
  }
584
 
585
 
586
+ def dataset_separation(
587
+ calibration_dataset: dict,
588
+ evaluation_dataset: dict,
589
+ ) -> dict[str, Any]:
590
+ """Describe whether model-selection images are absent from final evaluation."""
591
+ if not isinstance(calibration_dataset, dict) or not isinstance(
592
+ evaluation_dataset, dict
593
+ ):
594
+ raise TypeError("Dataset manifests must be dictionaries")
595
+
596
+ def file_hashes(dataset: dict, prefix: str) -> set[str]:
597
+ return {
598
+ str(record["sha256"])
599
+ for record in dataset.get("files", [])
600
+ if isinstance(record, dict)
601
+ and isinstance(record.get("path"), str)
602
+ and record["path"].startswith(prefix)
603
+ and isinstance(record.get("sha256"), str)
604
+ and record["sha256"]
605
+ }
606
+
607
+ calibration_images = file_hashes(calibration_dataset, "images/")
608
+ evaluation_images = file_hashes(evaluation_dataset, "images/")
609
+ calibration_text = file_hashes(calibration_dataset, "ground_truth/")
610
+ evaluation_text = file_hashes(evaluation_dataset, "ground_truth/")
611
+ image_overlap = sorted(calibration_images & evaluation_images)
612
+ ground_truth_overlap = sorted(calibration_text & evaluation_text)
613
+ distinct_digests = (
614
+ isinstance(calibration_dataset.get("content_sha256"), str)
615
+ and isinstance(evaluation_dataset.get("content_sha256"), str)
616
+ and calibration_dataset["content_sha256"]
617
+ != evaluation_dataset["content_sha256"]
618
+ )
619
+ return {
620
+ "passed": bool(calibration_images)
621
+ and bool(evaluation_images)
622
+ and bool(calibration_text)
623
+ and bool(evaluation_text)
624
+ and distinct_digests
625
+ and not image_overlap
626
+ and not ground_truth_overlap,
627
+ "calibration_digest": calibration_dataset.get("content_sha256"),
628
+ "evaluation_digest": evaluation_dataset.get("content_sha256"),
629
+ "overlapping_image_sha256": image_overlap,
630
+ "overlapping_ground_truth_sha256": ground_truth_overlap,
631
+ }
632
+
633
+
634
  def _pattern_is_within(child: str, parent: str) -> bool:
635
  """Return whether a precision rule is the parent itself or a descendant."""
636
  return child == parent or child.startswith(parent + ".")
637
 
638
 
639
+ def _finite_number(value: Any) -> float | None:
640
+ if (
641
+ not isinstance(value, (int, float))
642
+ or isinstance(value, bool)
643
+ or not math.isfinite(float(value))
644
+ ):
645
+ return None
646
+ return float(value)
647
+
648
+
649
  def generate_precision_map(
650
  base_map: dict,
651
  sensitivity_results: dict,
 
658
  """Turn group sensitivity measurements into executable top-level rules."""
659
  from quantization.layer_sensitivity import LAYER_GROUPS
660
 
661
+ for name, value in (
662
+ ("cer_threshold", cer_threshold),
663
+ ("digit_cer_threshold", digit_cer_threshold),
664
+ ("table_degradation_threshold", table_degradation_threshold),
665
+ ):
666
+ if _finite_number(value) is None or value < 0:
667
+ raise ValueError(f"{name} must be a finite non-negative number")
668
  groups = sensitivity_results.get("layer_groups")
669
  if not isinstance(groups, dict) or not groups:
670
  raise ValueError("Sensitivity results contain no layer_groups")
 
686
  ("digit_cer_delta", digit_cer_threshold),
687
  ("table_score_degradation", table_degradation_threshold),
688
  ):
689
+ value = _finite_number(result.get(key))
690
+ if value is None:
691
+ reasons.append(f"{key}=missing-or-non-finite")
692
+ elif value > threshold:
693
  reasons.append(f"{key}={value:.6f}")
694
  measured_values = [
695
+ _finite_number(result.get("cer_delta")),
696
+ _finite_number(result.get("digit_cer_delta")),
697
+ _finite_number(result.get("table_score_degradation")),
698
  ]
699
+ explicit_non_degradation = result.get("status") == "success" and all(
700
+ value is not None and value <= 0 for value in measured_values
 
 
 
 
 
 
 
 
 
701
  )
702
  if group_name in HARD_PROTECTED_GROUPS:
703
  reasons.append("protected OCR-critical group")
704
  elif group_name in EVIDENCE_PROTECTED_GROUPS and not explicit_non_degradation:
705
+ reasons.append(
706
+ "protected unless all measured quality deltas are non-degrading"
707
+ )
708
 
709
  retain_bf16 = bool(reasons)
710
  if retain_bf16:
 
721
  generated[group_pattern] = "mxfp8"
722
  decisions[group_name] = {
723
  "precision": (
724
+ "bfloat16"
725
+ if retain_bf16
726
+ else "mxfp8"
727
+ if group_name in EVIDENCE_PROTECTED_GROUPS
728
  else "base-map"
729
  ),
730
+ "reasons": reasons
731
+ or (
732
  ["all measured quality deltas were non-degrading"]
733
  if group_name in EVIDENCE_PROTECTED_GROUPS
734
  else ["within thresholds"]
 
740
  overrides = calibration_results.get("precision_overrides")
741
  if not isinstance(overrides, dict) or not overrides:
742
  raise ValueError("Calibration results contain no precision_overrides")
743
+ expected_pattern = "language_model.lm_head"
744
+ if (
745
+ set(overrides) != {expected_pattern}
746
+ or calibration_results.get("target_pattern") != expected_pattern
747
+ ):
748
+ raise ValueError("Calibration may override only language_model.lm_head")
749
+ selected = calibration_results.get("selected")
750
+ experiments = calibration_results.get("experiments")
751
+ if (
752
+ not isinstance(selected, dict)
753
+ or not isinstance(experiments, list)
754
+ or not experiments
755
+ ):
756
+ raise ValueError("Calibration selection evidence is incomplete")
757
  allowed_precisions = {"bfloat16", "mxfp8", "affine8"}
758
  for pattern, precision in overrides.items():
759
  if not isinstance(pattern, str) or precision not in allowed_precisions:
760
+ raise ValueError(
761
+ f"Invalid calibrated precision override: {pattern}={precision}"
762
+ )
763
+ matching = [
764
+ experiment
765
+ for experiment in experiments
766
+ if isinstance(experiment, dict)
767
+ and experiment.get("label") == selected.get("label")
768
+ and experiment.get("precision") == selected.get("precision")
769
+ and experiment.get("passed") is True
770
+ ]
771
+ if selected.get("precision") != precision or len(matching) != 1:
772
+ raise ValueError(
773
+ "Calibrated override does not match one passing experiment"
774
+ )
775
  generated[pattern] = precision
776
  for group_name, patterns in LAYER_GROUPS.items():
777
  if pattern in patterns:
 
805
  model_dir = Path(model_dir)
806
  config = load_json_object(model_dir / "config.json")
807
  processor = load_json_object(model_dir / "processor_config.json")
808
+ precision_map = load_json_object(model_dir / "precision_map.json")
809
+ summary = load_json_object(model_dir / "quantization_summary.json")
810
  architectures = config.get("architectures")
811
  quantization = config.get("quantization") or config.get("quantization_config")
812
  text_config = config.get("text_config") or config.get("language_config") or config
813
+ window = (
814
+ text_config.get("sliding_window_size")
815
+ if isinstance(text_config, dict)
816
+ else None
817
+ )
818
  if window is None and isinstance(text_config, dict):
819
  window = text_config.get("sliding_window")
820
+ precision_rules = {
821
+ pattern: precision
822
+ for pattern, precision in precision_map.items()
823
+ if not pattern.startswith("_")
824
+ }
825
+ quantized_modules = summary.get("quantized_modules")
826
+ module_precisions = summary.get("quantized_module_precisions")
827
+ precision_counts = summary.get("quantized_precision_counts")
828
+ valid_modules = (
829
+ isinstance(quantized_modules, list)
830
+ and bool(quantized_modules)
831
+ and all(isinstance(name, str) and bool(name) for name in quantized_modules)
832
+ and len(set(quantized_modules)) == len(quantized_modules)
833
+ and isinstance(module_precisions, dict)
834
+ and set(module_precisions) == set(quantized_modules)
835
+ and all(
836
+ precision in {"mxfp8", "affine8"}
837
+ for precision in module_precisions.values()
838
+ )
839
+ )
840
+ actual_precision_counts = (
841
+ dict(sorted(Counter(module_precisions.values()).items()))
842
+ if valid_modules
843
+ else None
844
+ )
845
+ tokenizer_config_path = model_dir / "tokenizer_config.json"
846
+ tokenizer_processor_ok = True
847
+ if tokenizer_config_path.is_file():
848
+ tokenizer_config = load_json_object(tokenizer_config_path)
849
+ tokenizer_processor_ok = (
850
+ tokenizer_config.get("processor_class") == "UnlimitedOCRHFProcessor"
851
+ )
852
+
853
  checks = {
854
+ "architecture": isinstance(architectures, list)
855
+ and "UnlimitedOCRForCausalLM" in architectures,
856
  "model_type": config.get("model_type") == "unlimited-ocr",
857
  "mxfp8": isinstance(quantization, dict) and quantization.get("mode") == "mxfp8",
858
+ "sliding_window": isinstance(window, int)
859
+ and not isinstance(window, bool)
860
+ and window > 0,
861
+ "processor_class": processor.get("processor_class")
862
+ == "UnlimitedOCRHFProcessor"
863
+ and tokenizer_processor_ok,
864
  "sft_format": processor.get("sft_format") == "unlimitedocr",
865
+ "precision_map": bool(precision_rules)
866
+ and all(
867
+ isinstance(pattern, str)
868
+ and bool(pattern)
869
+ and precision in {"bfloat16", "mxfp8", "affine8"}
870
+ for pattern, precision in precision_rules.items()
871
+ ),
872
+ "quantization_summary": summary.get("method") == "mxfp8"
873
+ and summary.get("group_size") == 32
874
+ and summary.get("bits") == 8
875
+ and isinstance(summary.get("source_model"), str)
876
+ and bool(summary["source_model"])
877
+ and isinstance(summary.get("source_revision"), str)
878
+ and bool(re.fullmatch(r"[0-9a-f]{40}", summary["source_revision"]))
879
+ and summary.get("precision_map_sha256") == _json_digest(precision_map)
880
+ and valid_modules
881
+ and summary.get("quantized_module_count") == len(quantized_modules)
882
+ and precision_counts == actual_precision_counts,
883
+ "protected_modules_preserved": valid_modules
884
+ and not any(
885
+ name.startswith(("sam_model.", "vision_model."))
886
+ or name.endswith(".mlp.gate")
887
+ for name in quantized_modules
888
+ ),
889
+ }
890
+ return {
891
+ "passed": all(checks.values()),
892
+ "checks": checks,
893
+ "sliding_window": window,
894
+ "source_model": summary.get("source_model"),
895
+ "source_revision": summary.get("source_revision"),
896
  }
 
897
 
898
 
899
  def _number(payload: dict, key: str) -> float | None:
900
+ return _finite_number(payload.get(key))
901
+
902
+
903
+ def validate_release_thresholds(thresholds: dict | None) -> dict[str, float | int]:
904
+ if thresholds is not None and not isinstance(thresholds, dict):
905
+ raise TypeError("thresholds must be a dictionary or None")
906
+ unexpected = sorted(set(thresholds or {}) - set(DEFAULT_THRESHOLDS))
907
+ if unexpected:
908
+ raise ValueError("Unknown release thresholds: " + ", ".join(unexpected))
909
+ limits = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
910
+ for name, value in limits.items():
911
+ if name == "min_rswa_tokens":
912
+ if not isinstance(value, int) or isinstance(value, bool) or value < 1:
913
+ raise ValueError("min_rswa_tokens must be a positive integer")
914
+ continue
915
+ number = _finite_number(value)
916
+ if number is None or number < 0:
917
+ raise ValueError(f"{name} must be a finite non-negative number")
918
+ limits[name] = number
919
+ if limits["max_rswa_repetition_rate"] > 1:
920
+ raise ValueError("max_rswa_repetition_rate must be in [0, 1]")
921
+ return limits
922
 
923
 
924
  def _gate(name: str, actual: Any, limit: Any, passed: bool, detail: str) -> dict:
925
+ return {
926
+ "name": name,
927
+ "passed": bool(passed),
928
+ "actual": actual,
929
+ "limit": limit,
930
+ "detail": detail,
931
+ }
932
+
933
+
934
+ def _positive_integer(payload: dict, key: str, *, minimum: int = 1) -> int | None:
935
+ value = payload.get(key)
936
+ if not isinstance(value, int) or isinstance(value, bool) or value < minimum:
937
+ return None
938
+ return value
939
+
940
+
941
+ def _accuracy_files(payload: dict) -> set[str] | None:
942
+ per_file = payload.get("per_file")
943
+ if not isinstance(per_file, list) or not per_file:
944
+ return None
945
+ names = [
946
+ item.get("file")
947
+ for item in per_file
948
+ if isinstance(item, dict) and isinstance(item.get("file"), str)
949
+ ]
950
+ return set(names) if len(names) == len(per_file) == len(set(names)) else None
951
+
952
+
953
+ def _official_mlx_accuracy_recipe(payload: dict) -> bool:
954
+ settings = payload.get("generation_settings")
955
+ return (
956
+ payload.get("backend") == "mlx"
957
+ and payload.get("prompt") == "<image>document parsing."
958
+ and payload.get("profile") == "accurate"
959
+ and _positive_integer(payload, "max_tokens") is not None
960
+ and isinstance(settings, dict)
961
+ and _number(settings, "temperature") == 0.0
962
+ and _number(settings, "top_p") == 1.0
963
+ and _number(settings, "repetition_penalty") == 1.0
964
+ and _positive_integer(settings, "no_repeat_ngram_size") == 35
965
+ and _positive_integer(settings, "ngram_window") == 128
966
+ )
967
+
968
+
969
+ def _performance_runs_complete(payload: dict) -> bool:
970
+ num_runs = _positive_integer(payload, "num_runs", minimum=3)
971
+ max_tokens = _positive_integer(payload, "max_tokens")
972
+ runs = payload.get("runs")
973
+ if (
974
+ num_runs is None
975
+ or max_tokens is None
976
+ or not isinstance(runs, list)
977
+ or len(runs) != num_runs
978
+ ):
979
+ return False
980
+ return all(
981
+ isinstance(run, dict)
982
+ and _positive_integer(run, "run") == index
983
+ and _positive_integer(run, "tokens_generated") == max_tokens
984
+ and run.get("tokens_generated_source") == "mlx-vlm token count"
985
+ and (_number(run, "tokens_per_second") or 0) > 0
986
+ and run.get("tokens_per_second_source") == "mlx-vlm generation_tps"
987
+ and (_number(run, "elapsed_seconds") or 0) > 0
988
+ and run.get("finish_reason") == "length"
989
+ for index, run in enumerate(runs, start=1)
990
+ )
991
+
992
+
993
+ def _mean_matches(value: object, values: list[float]) -> bool:
994
+ number = _finite_number(value)
995
+ if number is None or not values:
996
+ return False
997
+ expected = sum(values) / len(values)
998
+ return math.isclose(number, expected, rel_tol=1e-12, abs_tol=1e-12)
999
+
1000
+
1001
+ def _accuracy_aggregates_valid(payload: dict) -> bool:
1002
+ """Recompute every release-relevant accuracy aggregate from per-file rows."""
1003
+ per_file = payload.get("per_file")
1004
+ if not isinstance(per_file, list) or not per_file:
1005
+ return False
1006
+ cer_values = []
1007
+ digit_values = []
1008
+ cjk_values = []
1009
+ table_values = []
1010
+ repetition_values = []
1011
+ elapsed_values = []
1012
+ for item in per_file:
1013
+ if not isinstance(item, dict):
1014
+ return False
1015
+ cer = _number(item, "cer")
1016
+ digit_cer = _number(item, "digit_cer")
1017
+ cjk_cer = _number(item, "cjk_cer")
1018
+ repetition = _number(item, "repetition_rate")
1019
+ elapsed = _number(item, "elapsed_seconds")
1020
+ digit_count = item.get("ref_digit_count")
1021
+ cjk_count = item.get("ref_cjk_count")
1022
+ table_score = item.get("table_score")
1023
+ if (
1024
+ cer is None
1025
+ or cer < 0
1026
+ or digit_cer is None
1027
+ or digit_cer < 0
1028
+ or cjk_cer is None
1029
+ or cjk_cer < 0
1030
+ or repetition is None
1031
+ or not 0 <= repetition <= 1
1032
+ or elapsed is None
1033
+ or elapsed < 0
1034
+ or not isinstance(digit_count, int)
1035
+ or isinstance(digit_count, bool)
1036
+ or digit_count < 0
1037
+ or not isinstance(cjk_count, int)
1038
+ or isinstance(cjk_count, bool)
1039
+ or cjk_count < 0
1040
+ ):
1041
+ return False
1042
+ cer_values.append(cer)
1043
+ repetition_values.append(repetition)
1044
+ elapsed_values.append(elapsed)
1045
+ if digit_count > 0:
1046
+ digit_values.append(digit_cer)
1047
+ if cjk_count > 0:
1048
+ cjk_values.append(cjk_cer)
1049
+ if table_score is not None:
1050
+ score = _finite_number(table_score)
1051
+ if score is None or not 0 <= score <= 1:
1052
+ return False
1053
+ table_values.append(score)
1054
+
1055
+ total_time = _number(payload, "total_time_seconds")
1056
+ return (
1057
+ payload.get("num_images") == len(per_file)
1058
+ and payload.get("num_samples") == len(per_file)
1059
+ and payload.get("num_digit_samples") == len(digit_values)
1060
+ and payload.get("num_cjk_samples") == len(cjk_values)
1061
+ and payload.get("num_table_samples") == len(table_values)
1062
+ and _mean_matches(payload.get("mean_cer"), cer_values)
1063
+ and _mean_matches(payload.get("mean_digit_cer"), digit_values)
1064
+ and _mean_matches(payload.get("mean_cjk_cer"), cjk_values)
1065
+ and _mean_matches(payload.get("mean_table_score"), table_values)
1066
+ and _mean_matches(payload.get("mean_repetition_rate"), repetition_values)
1067
+ and total_time is not None
1068
+ and math.isclose(
1069
+ total_time,
1070
+ sum(elapsed_values),
1071
+ rel_tol=1e-12,
1072
+ abs_tol=1e-12,
1073
+ )
1074
+ )
1075
+
1076
+
1077
+ def _performance_aggregates_valid(payload: dict) -> bool:
1078
+ if not _performance_runs_complete(payload):
1079
+ return False
1080
+ runs = payload["runs"]
1081
+ tps_values = [_number(run, "tokens_per_second") for run in runs]
1082
+ elapsed_values = [_number(run, "elapsed_seconds") for run in runs]
1083
+ memory_values = [_number(run, "peak_memory_mb") for run in runs]
1084
+ if (
1085
+ any(value is None or value <= 0 for value in tps_values)
1086
+ or any(value is None or value <= 0 for value in elapsed_values)
1087
+ or any(value is None or value < 0 for value in memory_values)
1088
+ ):
1089
+ return False
1090
+ return (
1091
+ _mean_matches(payload.get("mean_tps"), tps_values)
1092
+ and _mean_matches(payload.get("mean_elapsed_seconds"), elapsed_values)
1093
+ and _mean_matches(payload.get("mean_peak_memory_mb"), memory_values)
1094
+ )
1095
+
1096
+
1097
+ def _calibration_artifact_record(path: Path) -> dict:
1098
+ return {
1099
+ "filename": path.name,
1100
+ "size": path.stat().st_size,
1101
+ "sha256": sha256_file(path),
1102
+ }
1103
+
1104
+
1105
+ def validate_calibration_results(
1106
+ calibration: dict,
1107
+ *,
1108
+ bf16_accuracy: dict,
1109
+ reference_performance: dict,
1110
+ calibration_dataset: dict,
1111
+ source_revision: str,
1112
+ reference_revision: str,
1113
+ evidence: dict[str, dict],
1114
+ evidence_paths: dict[str, str | Path],
1115
+ thresholds: dict | None = None,
1116
+ ) -> dict:
1117
+ """Recompute calibration selection from content-addressed raw evidence."""
1118
+ limits = validate_release_thresholds(thresholds)
1119
+ if not re.fullmatch(r"[0-9a-f]{40}", source_revision):
1120
+ raise ValueError("Calibration source revision must be a lowercase commit SHA")
1121
+ if not re.fullmatch(r"[0-9a-f]{40}", reference_revision):
1122
+ raise ValueError(
1123
+ "Calibration reference revision must be a lowercase commit SHA"
1124
+ )
1125
+ if calibration.get("schema_version") != 3:
1126
+ raise ValueError("Calibration schema_version 3 is required")
1127
+ if calibration.get("target_pattern") != "language_model.lm_head":
1128
+ raise ValueError("Calibration may target only language_model.lm_head")
1129
+ if calibration.get("thresholds") != limits:
1130
+ raise ValueError("Calibration thresholds do not match release thresholds")
1131
+ if calibration.get("selection_policy") != (
1132
+ "fastest candidate passing existing quality and throughput limits"
1133
+ ):
1134
+ raise ValueError("Calibration selection policy is invalid")
1135
+
1136
+ expected_input_keys = {
1137
+ "calibration_baseline_accuracy",
1138
+ "calibration_reference_performance",
1139
+ *CALIBRATION_RAW_EVIDENCE_NAMES,
1140
+ }
1141
+ input_artifacts = calibration.get("input_artifacts")
1142
+ if (
1143
+ not isinstance(input_artifacts, dict)
1144
+ or set(input_artifacts) != expected_input_keys
1145
+ ):
1146
+ raise ValueError("Calibration input artifacts are incomplete")
1147
+ for name in expected_input_keys:
1148
+ path = Path(evidence_paths[name])
1149
+ if path.is_symlink() or not path.is_file():
1150
+ raise FileNotFoundError(f"Calibration input is missing: {name}")
1151
+ if input_artifacts.get(name) != _calibration_artifact_record(path):
1152
+ raise ValueError(f"Calibration input hash does not match: {name}")
1153
+ if (
1154
+ evidence.get("calibration_baseline_accuracy") != bf16_accuracy
1155
+ or evidence.get("calibration_reference_performance")
1156
+ != reference_performance
1157
+ ):
1158
+ raise ValueError("Calibration baselines do not match raw evidence")
1159
+
1160
+ if not isinstance(calibration_dataset, dict) or calibration.get(
1161
+ "dataset"
1162
+ ) != calibration_dataset:
1163
+ raise ValueError("Calibration dataset provenance does not match")
1164
+ expected_files = {
1165
+ Path(record["path"]).name
1166
+ for record in calibration_dataset.get("files", [])
1167
+ if isinstance(record, dict)
1168
+ and isinstance(record.get("path"), str)
1169
+ and record["path"].startswith("images/")
1170
+ }
1171
+ if (
1172
+ not expected_files
1173
+ or len(expected_files) != calibration_dataset.get("num_samples")
1174
+ or _accuracy_files(bf16_accuracy) != expected_files
1175
+ or bf16_accuracy.get("num_images") != calibration_dataset.get("num_samples")
1176
+ or bf16_accuracy.get("num_samples") != calibration_dataset.get("num_samples")
1177
+ or bf16_accuracy.get("num_digit_samples")
1178
+ != calibration_dataset.get("num_digit_samples")
1179
+ or bf16_accuracy.get("num_cjk_samples")
1180
+ != calibration_dataset.get("num_cjk_samples")
1181
+ or bf16_accuracy.get("num_table_samples")
1182
+ != calibration_dataset.get("num_table_samples")
1183
+ ):
1184
+ raise ValueError("Calibration baseline does not match its dataset")
1185
+
1186
+ baseline_values = {
1187
+ "mean_cer": _number(bf16_accuracy, "mean_cer"),
1188
+ "mean_digit_cer": _number(bf16_accuracy, "mean_digit_cer"),
1189
+ "mean_table_score": _number(bf16_accuracy, "mean_table_score"),
1190
+ }
1191
+ reference_tps = _number(reference_performance, "mean_tps")
1192
+ if (
1193
+ any(value is None for value in baseline_values.values())
1194
+ or reference_tps is None
1195
+ or reference_tps <= 0
1196
+ or not _accuracy_aggregates_valid(bf16_accuracy)
1197
+ or not _performance_aggregates_valid(reference_performance)
1198
+ or bf16_accuracy.get("served_revision") != source_revision
1199
+ or reference_performance.get("served_revision") != reference_revision
1200
+ ):
1201
+ raise ValueError("Calibration baselines are incomplete or inconsistent")
1202
+
1203
+ experiments = calibration.get("experiments")
1204
+ if not isinstance(experiments, list) or len(experiments) != len(
1205
+ CALIBRATION_EVIDENCE_KEYS
1206
+ ):
1207
+ raise ValueError("Calibration must contain exactly three experiments")
1208
+ stored_by_precision = {}
1209
+ labels = set()
1210
+ for experiment in experiments:
1211
+ if not isinstance(experiment, dict):
1212
+ raise ValueError("Calibration experiment must be an object")
1213
+ precision = experiment.get("precision")
1214
+ label = experiment.get("label")
1215
+ if (
1216
+ precision not in CALIBRATION_EVIDENCE_KEYS
1217
+ or precision in stored_by_precision
1218
+ or not isinstance(label, str)
1219
+ or not label
1220
+ or label != label.strip()
1221
+ or label in labels
1222
+ ):
1223
+ raise ValueError("Calibration experiment identities are invalid")
1224
+ stored_by_precision[precision] = experiment
1225
+ labels.add(label)
1226
+ if set(stored_by_precision) != set(CALIBRATION_EVIDENCE_KEYS):
1227
+ raise ValueError("Calibration precision coverage is incomplete")
1228
+
1229
+ expected_experiments = []
1230
+ model_paths = set()
1231
+ accuracy_recipe_keys = ("prompt", "max_tokens", "profile", "generation_settings")
1232
+ performance_recipe_keys = (
1233
+ "image_path",
1234
+ "prompt",
1235
+ "max_tokens",
1236
+ "num_warmup",
1237
+ "num_runs",
1238
+ "system",
1239
+ )
1240
+ baseline_files = _accuracy_files(bf16_accuracy)
1241
+ for precision, (accuracy_key, performance_key) in CALIBRATION_EVIDENCE_KEYS.items():
1242
+ accuracy = evidence.get(accuracy_key)
1243
+ performance = evidence.get(performance_key)
1244
+ if not isinstance(accuracy, dict) or not isinstance(performance, dict):
1245
+ raise ValueError(f"Calibration raw evidence is missing for {precision}")
1246
+ model_path = accuracy.get("model_path")
1247
+ if (
1248
+ not isinstance(model_path, str)
1249
+ or not model_path
1250
+ or performance.get("model_path") != model_path
1251
+ or performance.get("served_revision")
1252
+ != accuracy.get("served_revision")
1253
+ or model_path in model_paths
1254
+ ):
1255
+ raise ValueError(f"Calibration model identity is invalid for {precision}")
1256
+ model_paths.add(model_path)
1257
+ if (
1258
+ not _official_mlx_accuracy_recipe(accuracy)
1259
+ or not _accuracy_aggregates_valid(accuracy)
1260
+ or _accuracy_files(accuracy) != baseline_files
1261
+ or any(
1262
+ accuracy.get(key) != bf16_accuracy.get(key)
1263
+ for key in accuracy_recipe_keys
1264
+ )
1265
+ or any(
1266
+ accuracy.get(key) != bf16_accuracy.get(key)
1267
+ for key in (
1268
+ "num_images",
1269
+ "num_samples",
1270
+ "num_digit_samples",
1271
+ "num_cjk_samples",
1272
+ "num_table_samples",
1273
+ )
1274
+ )
1275
+ ):
1276
+ raise ValueError(f"Calibration accuracy recipe is invalid for {precision}")
1277
+ if not _performance_aggregates_valid(performance) or any(
1278
+ performance.get(key) != reference_performance.get(key)
1279
+ for key in performance_recipe_keys
1280
+ ):
1281
+ raise ValueError(
1282
+ f"Calibration performance recipe is invalid for {precision}"
1283
+ )
1284
+
1285
+ candidate_cer = _number(accuracy, "mean_cer")
1286
+ candidate_digit = _number(accuracy, "mean_digit_cer")
1287
+ candidate_table = _number(accuracy, "mean_table_score")
1288
+ candidate_tps = _number(performance, "mean_tps")
1289
+ if None in (candidate_cer, candidate_digit, candidate_table, candidate_tps):
1290
+ raise ValueError(f"Calibration metrics are incomplete for {precision}")
1291
+ deltas = {
1292
+ "cer_vs_bf16": candidate_cer - baseline_values["mean_cer"],
1293
+ "digit_cer_vs_bf16": (candidate_digit - baseline_values["mean_digit_cer"]),
1294
+ "table_degradation_vs_bf16": (
1295
+ baseline_values["mean_table_score"] - candidate_table
1296
+ ),
1297
+ "tps_ratio_vs_reference": candidate_tps / reference_tps,
1298
+ }
1299
+ checks = {
1300
+ "cer": deltas["cer_vs_bf16"] <= limits["max_cer_delta_vs_bf16"],
1301
+ "digit_cer": deltas["digit_cer_vs_bf16"]
1302
+ <= limits["max_digit_cer_delta_vs_bf16"],
1303
+ "table_score": deltas["table_degradation_vs_bf16"]
1304
+ <= limits["max_table_score_degradation_vs_bf16"],
1305
+ "throughput": deltas["tps_ratio_vs_reference"]
1306
+ >= limits["min_tps_ratio_vs_reference"],
1307
+ }
1308
+ expected = {
1309
+ "label": stored_by_precision[precision]["label"],
1310
+ "precision": precision,
1311
+ "passed": all(checks.values()),
1312
+ "checks": checks,
1313
+ "metrics": {
1314
+ "mean_cer": candidate_cer,
1315
+ "mean_digit_cer": candidate_digit,
1316
+ "mean_table_score": candidate_table,
1317
+ "mean_tps": candidate_tps,
1318
+ },
1319
+ "deltas": deltas,
1320
+ }
1321
+ if stored_by_precision[precision] != expected:
1322
+ raise ValueError(f"Calibration claims do not recompute for {precision}")
1323
+ expected_experiments.append(expected)
1324
+
1325
+ passing = [
1326
+ experiment for experiment in expected_experiments if experiment["passed"]
1327
+ ]
1328
+ if not passing:
1329
+ raise ValueError("No calibration experiment passes every release limit")
1330
+ selected_experiment = max(
1331
+ passing,
1332
+ key=lambda experiment: experiment["metrics"]["mean_tps"],
1333
+ )
1334
+ selected = {
1335
+ "label": selected_experiment["label"],
1336
+ "precision": selected_experiment["precision"],
1337
+ }
1338
+ if calibration.get("selected") != selected:
1339
+ raise ValueError("Calibration did not select the fastest passing experiment")
1340
+ if calibration.get("precision_overrides") != {
1341
+ "language_model.lm_head": selected["precision"]
1342
+ }:
1343
+ raise ValueError("Calibration precision override does not match selection")
1344
+ return {
1345
+ "selected": selected,
1346
+ "models": sorted(model_paths),
1347
+ "dataset_digest": calibration_dataset.get("content_sha256"),
1348
+ "input_artifacts": input_artifacts,
1349
+ }
1350
 
1351
 
1352
  def evaluate_release_gates(
 
1354
  candidate_weights: dict,
1355
  reference_weights: dict,
1356
  dataset: dict,
1357
+ calibration_dataset: dict,
1358
  metadata: dict,
1359
  bf16_accuracy: dict,
1360
  reference_accuracy: dict,
 
1362
  reference_performance: dict,
1363
  candidate_performance: dict,
1364
  rswa: dict,
1365
+ provenance: dict,
1366
+ source_model_name: str,
1367
+ source_id: str,
1368
+ source_config_sha256: str,
1369
+ source_revision: str,
1370
+ reference_id: str,
1371
+ reference_revision: str,
1372
+ candidate_model_name: str,
1373
+ repo_id: str,
1374
  thresholds: dict | None = None,
1375
  ) -> list[dict]:
1376
  """Evaluate every required release gate; missing values fail closed."""
1377
+ limits = validate_release_thresholds(thresholds)
1378
  gates = []
1379
  candidate_digest = candidate_weights.get("aggregate_sha256")
1380
  reference_digest = reference_weights.get("aggregate_sha256")
1381
+ gates.append(
1382
+ _gate(
1383
+ "weights_are_distinct",
1384
+ candidate_digest,
1385
+ f"different from {reference_digest}",
1386
+ bool(
1387
+ candidate_digest
1388
+ and reference_digest
1389
+ and candidate_digest != reference_digest
1390
+ ),
1391
+ "Candidate aggregate digest must differ from the Sahil reference",
1392
+ )
1393
+ )
1394
  candidate_size = _number(candidate_weights, "total_size_gb")
1395
+ gates.append(
1396
+ _gate(
1397
+ "weight_size_gb",
1398
+ candidate_size,
1399
+ limits["max_weight_size_gb"],
1400
+ candidate_size is not None
1401
+ and candidate_size <= limits["max_weight_size_gb"],
1402
+ "Candidate Safetensors size",
1403
+ )
1404
+ )
1405
+ gates.append(
1406
+ _gate(
1407
+ "native_model_metadata",
1408
+ metadata.get("checks"),
1409
+ True,
1410
+ metadata.get("passed") is True,
1411
+ "Native Unlimited-OCR, MXFP8, and R-SWA metadata",
1412
+ )
1413
+ )
1414
+ candidate_source = {
1415
+ "model": metadata.get("source_model"),
1416
+ "revision": metadata.get("source_revision"),
1417
+ }
1418
+ gates.append(
1419
+ _gate(
1420
+ "candidate_source_provenance",
1421
+ candidate_source,
1422
+ {
1423
+ "model": [source_id, source_model_name],
1424
+ "revision": source_revision,
1425
+ },
1426
+ candidate_source["model"] in {source_id, source_model_name}
1427
+ and candidate_source["revision"] == source_revision,
1428
+ "Converted metadata must bind the exact BF16 source commit",
1429
+ )
1430
+ )
1431
 
1432
  coverage = {
1433
  "samples": dataset.get("num_samples", 0),
 
1435
  "cjk": dataset.get("num_cjk_samples", 0),
1436
  "table": dataset.get("num_table_samples", 0),
1437
  }
1438
+ gates.append(
1439
+ _gate(
1440
+ "evaluation_coverage",
1441
+ coverage,
1442
+ "all counts > 0",
1443
+ all(
1444
+ isinstance(value, int) and not isinstance(value, bool) and value > 0
1445
+ for value in coverage.values()
1446
+ ),
1447
+ "Dataset must cover ordinary text, digits, CJK, and tables",
1448
+ )
1449
+ )
1450
+ separation = dataset_separation(calibration_dataset, dataset)
1451
+ gates.append(
1452
+ _gate(
1453
+ "held_out_evaluation_dataset",
1454
+ separation,
1455
+ "distinct dataset digests and no shared image or ground-truth hashes",
1456
+ separation["passed"],
1457
+ "Final quality evidence must not reuse model-selection samples",
1458
+ )
1459
+ )
1460
+ expected_files = {
1461
+ Path(item["path"]).name
1462
+ for item in dataset.get("files", [])
1463
+ if isinstance(item, dict)
1464
+ and isinstance(item.get("path"), str)
1465
+ and item["path"].startswith("images/")
1466
+ }
1467
+ accuracy_payloads = (
1468
+ bf16_accuracy,
1469
+ reference_accuracy,
1470
+ candidate_accuracy,
1471
+ )
1472
+ gates.append(
1473
+ _gate(
1474
+ "accuracy_aggregates_recomputed",
1475
+ [_accuracy_aggregates_valid(payload) for payload in accuracy_payloads],
1476
+ [True, True, True],
1477
+ all(_accuracy_aggregates_valid(payload) for payload in accuracy_payloads),
1478
+ "Every reported accuracy aggregate must recompute from per-file rows",
1479
+ )
1480
+ )
1481
+ sample_counts = [payload.get("num_samples") for payload in accuracy_payloads]
1482
+ evaluated_files = [_accuracy_files(payload) for payload in accuracy_payloads]
1483
+ gates.append(
1484
+ _gate(
1485
+ "same_evaluation_samples",
1486
+ {
1487
+ "counts": sample_counts,
1488
+ "file_counts": [len(files or set()) for files in evaluated_files],
1489
+ },
1490
+ {"count": dataset.get("num_samples"), "files": sorted(expected_files)},
1491
+ bool(expected_files)
1492
+ and len(expected_files) == dataset.get("num_samples")
1493
+ and all(count == dataset.get("num_samples") for count in sample_counts)
1494
+ and all(
1495
+ payload.get("num_images") == dataset.get("num_samples")
1496
+ for payload in accuracy_payloads
1497
+ )
1498
+ and all(files == expected_files for files in evaluated_files),
1499
+ "All three checkpoints must run every identical evaluation file",
1500
+ )
1501
+ )
1502
+ model_identities = {
1503
+ "bf16": bf16_accuracy.get("model_path"),
1504
+ "reference": reference_accuracy.get("model_path"),
1505
+ "candidate": candidate_accuracy.get("model_path"),
1506
+ "reference_performance": reference_performance.get("model_path"),
1507
+ "candidate_performance": candidate_performance.get("model_path"),
1508
+ "rswa": rswa.get("model_path"),
1509
+ }
1510
+ gates.append(
1511
+ _gate(
1512
+ "model_identities",
1513
+ model_identities,
1514
+ {
1515
+ "bf16": source_model_name,
1516
+ "reference": reference_id,
1517
+ "candidate": candidate_model_name,
1518
+ },
1519
+ model_identities
1520
+ == {
1521
+ "bf16": source_model_name,
1522
+ "reference": reference_id,
1523
+ "candidate": candidate_model_name,
1524
+ "reference_performance": reference_id,
1525
+ "candidate_performance": candidate_model_name,
1526
+ "rswa": candidate_model_name,
1527
+ },
1528
+ "Accuracy, performance, and R-SWA evidence must identify exact models",
1529
+ )
1530
+ )
1531
+ model_revisions = {
1532
+ "bf16_accuracy": bf16_accuracy.get("served_revision"),
1533
+ "reference_accuracy": reference_accuracy.get("served_revision"),
1534
+ "candidate_accuracy": candidate_accuracy.get("served_revision"),
1535
+ "reference_performance": reference_performance.get("served_revision"),
1536
+ "candidate_performance": candidate_performance.get("served_revision"),
1537
+ }
1538
+ expected_revisions = {
1539
+ "bf16_accuracy": source_revision,
1540
+ "reference_accuracy": reference_revision,
1541
+ "candidate_accuracy": None,
1542
+ "reference_performance": reference_revision,
1543
+ "candidate_performance": None,
1544
+ }
1545
+ gates.append(
1546
+ _gate(
1547
+ "immutable_model_revisions",
1548
+ model_revisions,
1549
+ expected_revisions,
1550
+ bool(re.fullmatch(r"[0-9a-f]{40}", source_revision))
1551
+ and bool(re.fullmatch(r"[0-9a-f]{40}", reference_revision))
1552
+ and model_revisions == expected_revisions,
1553
+ "Remote source and reference evidence must name exact Hub commits",
1554
+ )
1555
+ )
1556
+ accuracy_recipe_keys = ("prompt", "max_tokens", "profile", "generation_settings")
1557
+ gates.append(
1558
+ _gate(
1559
+ "same_accuracy_recipe",
1560
+ {
1561
+ key: [payload.get(key) for payload in accuracy_payloads]
1562
+ for key in accuracy_recipe_keys
1563
+ },
1564
+ "identical official MLX OCR recipe",
1565
+ all(_official_mlx_accuracy_recipe(payload) for payload in accuracy_payloads)
1566
+ and all(
1567
+ payload.get(key) == bf16_accuracy.get(key)
1568
+ for payload in accuracy_payloads[1:]
1569
+ for key in accuracy_recipe_keys
1570
+ ),
1571
+ "Accuracy runs must use the same deterministic OCR generation settings",
1572
+ )
1573
+ )
1574
 
1575
  bf16_cer = _number(bf16_accuracy, "mean_cer")
1576
  reference_cer = _number(reference_accuracy, "mean_cer")
1577
  candidate_cer = _number(candidate_accuracy, "mean_cer")
1578
+ delta_bf16 = (
1579
+ candidate_cer - bf16_cer
1580
+ if candidate_cer is not None and bf16_cer is not None
1581
+ else None
1582
+ )
1583
+ delta_reference = (
1584
+ candidate_cer - reference_cer
1585
+ if candidate_cer is not None and reference_cer is not None
1586
+ else None
1587
+ )
1588
+ gates.append(
1589
+ _gate(
1590
+ "candidate_cer_vs_bf16",
1591
+ delta_bf16,
1592
+ limits["max_cer_delta_vs_bf16"],
1593
+ delta_bf16 is not None and delta_bf16 <= limits["max_cer_delta_vs_bf16"],
1594
+ "Candidate minus BF16 absolute mean CER",
1595
+ )
1596
+ )
1597
+ gates.append(
1598
+ _gate(
1599
+ "candidate_cer_vs_reference",
1600
+ delta_reference,
1601
+ limits["max_cer_delta_vs_reference"],
1602
+ delta_reference is not None
1603
+ and delta_reference <= limits["max_cer_delta_vs_reference"],
1604
+ "Candidate minus Sahil-reference absolute mean CER",
1605
+ )
1606
+ )
1607
 
1608
  bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
1609
  candidate_digit = _number(candidate_accuracy, "mean_digit_cer")
1610
+ digit_delta = (
1611
+ candidate_digit - bf16_digit
1612
+ if candidate_digit is not None and bf16_digit is not None
1613
+ else None
1614
+ )
1615
+ gates.append(
1616
+ _gate(
1617
+ "candidate_digit_cer_vs_bf16",
1618
+ digit_delta,
1619
+ limits["max_digit_cer_delta_vs_bf16"],
1620
+ digit_delta is not None
1621
+ and digit_delta <= limits["max_digit_cer_delta_vs_bf16"],
1622
+ "Candidate minus BF16 digit CER",
1623
+ )
1624
+ )
1625
 
1626
  bf16_table = _number(bf16_accuracy, "mean_table_score")
1627
  candidate_table = _number(candidate_accuracy, "mean_table_score")
1628
+ table_degradation = (
1629
+ bf16_table - candidate_table
1630
+ if bf16_table is not None and candidate_table is not None
1631
+ else None
1632
+ )
1633
+ gates.append(
1634
+ _gate(
1635
+ "candidate_table_score_vs_bf16",
1636
+ table_degradation,
1637
+ limits["max_table_score_degradation_vs_bf16"],
1638
+ table_degradation is not None
1639
+ and table_degradation <= limits["max_table_score_degradation_vs_bf16"],
1640
+ "BF16 minus candidate mean table score",
1641
+ )
1642
+ )
1643
+
1644
+ performance_keys = (
1645
+ "image_path",
1646
+ "prompt",
1647
+ "max_tokens",
1648
+ "num_warmup",
1649
+ "num_runs",
1650
+ "system",
1651
+ )
1652
+ gates.append(
1653
+ _gate(
1654
+ "performance_aggregates_recomputed",
1655
+ {
1656
+ "reference": _performance_aggregates_valid(reference_performance),
1657
+ "candidate": _performance_aggregates_valid(candidate_performance),
1658
+ },
1659
+ {"reference": True, "candidate": True},
1660
+ _performance_aggregates_valid(reference_performance)
1661
+ and _performance_aggregates_valid(candidate_performance),
1662
+ "Performance means must recompute from complete benchmark runs",
1663
+ )
1664
+ )
1665
+ gates.append(
1666
+ _gate(
1667
+ "same_performance_setup",
1668
+ {
1669
+ key: [reference_performance.get(key), candidate_performance.get(key)]
1670
+ for key in performance_keys
1671
+ },
1672
+ "identical setup with at least three complete runs",
1673
+ all(
1674
+ reference_performance.get(key) == candidate_performance.get(key)
1675
+ for key in performance_keys
1676
+ )
1677
+ and reference_performance.get("prompt") == "<image>document parsing."
1678
+ and _positive_integer(reference_performance, "max_tokens") is not None
1679
+ and _positive_integer(reference_performance, "num_warmup") is not None
1680
+ and _performance_runs_complete(reference_performance)
1681
+ and _performance_runs_complete(candidate_performance)
1682
+ and isinstance(reference_performance.get("system"), dict)
1683
+ and bool(reference_performance["system"]),
1684
+ "Performance comparisons must use the same host and benchmark recipe",
1685
+ )
1686
+ )
1687
 
1688
  reference_tps = _number(reference_performance, "mean_tps")
1689
  candidate_tps = _number(candidate_performance, "mean_tps")
1690
+ tps_ratio = (
1691
+ candidate_tps / reference_tps
1692
+ if candidate_tps is not None and reference_tps and reference_tps > 0
1693
+ else None
1694
+ )
1695
+ gates.append(
1696
+ _gate(
1697
+ "candidate_tps_vs_reference",
1698
+ tps_ratio,
1699
+ limits["min_tps_ratio_vs_reference"],
1700
+ tps_ratio is not None and tps_ratio >= limits["min_tps_ratio_vs_reference"],
1701
+ "Candidate decode throughput divided by Sahil-reference throughput",
1702
+ )
1703
+ )
1704
+ rswa_results = rswa.get("test_results")
1705
+ long_results = (
1706
+ [
1707
+ result
1708
+ for result in rswa_results
1709
+ if isinstance(rswa_results, list)
1710
+ and isinstance(result, dict)
1711
+ and _positive_integer(result, "max_tokens") == limits["min_rswa_tokens"]
1712
+ ]
1713
+ if isinstance(rswa_results, list)
1714
+ else []
1715
+ )
1716
+ long_result = long_results[0] if len(long_results) == 1 else {}
1717
+ repetition_rate = _number(long_result, "repetition_rate")
1718
+ forced_tokens = _positive_integer(rswa, "force_min_tokens")
1719
+ generated_tokens = _positive_integer(long_result, "tokens_generated")
1720
+ pass_conditions = rswa.get("pass_conditions")
1721
+ rswa_generation = rswa.get("generation_settings")
1722
+ gates.append(
1723
+ _gate(
1724
+ "rswa_8k_bounded",
1725
+ {
1726
+ "pass_conditions": pass_conditions,
1727
+ "tokens": long_result.get("tokens_generated"),
1728
+ "repetition_rate": repetition_rate,
1729
+ },
1730
+ {
1731
+ "min_tokens": limits["min_rswa_tokens"],
1732
+ "max_repetition_rate": limits["max_rswa_repetition_rate"],
1733
+ },
1734
+ rswa.get("passed") is True
1735
+ and isinstance(pass_conditions, dict)
1736
+ and pass_conditions.get("cache_bounded") is True
1737
+ and pass_conditions.get("tps_stable") is True
1738
+ and pass_conditions.get("8k_test_passed") is True
1739
+ and rswa.get("prompt") == "<image>document parsing."
1740
+ and isinstance(rswa_generation, dict)
1741
+ and _number(rswa_generation, "temperature") == 0.0
1742
+ and _positive_integer(rswa_generation, "no_repeat_ngram_size") == 35
1743
+ and _positive_integer(rswa_generation, "ngram_window") == 128
1744
+ and forced_tokens is not None
1745
+ and forced_tokens >= limits["min_rswa_tokens"]
1746
+ and len(long_results) == 1
1747
+ and long_result.get("status") == "success"
1748
+ and long_result.get("tokens_generated_reliable") is True
1749
+ and generated_tokens is not None
1750
+ and generated_tokens >= limits["min_rswa_tokens"]
1751
+ and repetition_rate is not None
1752
+ and 0 <= repetition_rate <= limits["max_rswa_repetition_rate"],
1753
+ "8K generation must have bounded cache, stable throughput, and repetition",
1754
+ )
1755
+ )
1756
+ provenance_dataset = provenance.get("evaluation_dataset")
1757
+ provenance_calibration_dataset = provenance.get("calibration_dataset")
1758
+ source_config = provenance.get("source_config")
1759
+ gates.append(
1760
+ _gate(
1761
+ "provenance_matches_release",
1762
+ {
1763
+ "source": provenance.get("source_model"),
1764
+ "reference": provenance.get("reference_model"),
1765
+ "target": provenance.get("target_repo"),
1766
+ "source_config": source_config,
1767
+ "source_revision": provenance.get("source_revision"),
1768
+ "reference_revision": provenance.get("reference_revision"),
1769
+ "dataset_digest": (
1770
+ provenance_dataset.get("content_sha256")
1771
+ if isinstance(provenance_dataset, dict)
1772
+ else None
1773
+ ),
1774
+ "calibration_dataset_digest": (
1775
+ provenance_calibration_dataset.get("content_sha256")
1776
+ if isinstance(provenance_calibration_dataset, dict)
1777
+ else None
1778
+ ),
1779
+ },
1780
+ {
1781
+ "source": source_id,
1782
+ "reference": reference_id,
1783
+ "target": repo_id,
1784
+ "source_config_sha256": source_config_sha256,
1785
+ "source_revision": source_revision,
1786
+ "reference_revision": reference_revision,
1787
+ "dataset_digest": dataset.get("content_sha256"),
1788
+ "calibration_dataset_digest": calibration_dataset.get(
1789
+ "content_sha256"
1790
+ ),
1791
+ },
1792
+ provenance.get("source_model") == source_id
1793
+ and provenance.get("reference_model") == reference_id
1794
+ and provenance.get("target_repo") == repo_id
1795
+ and provenance.get("source_revision") == source_revision
1796
+ and provenance.get("reference_revision") == reference_revision
1797
+ and isinstance(source_config, dict)
1798
+ and source_config.get("path") == "config.json"
1799
+ and source_config.get("sha256") == source_config_sha256
1800
+ and provenance_dataset == dataset
1801
+ and provenance_calibration_dataset == calibration_dataset,
1802
+ "Provenance must bind the source, selection/evaluation datasets, and target",
1803
+ )
1804
+ )
1805
  return gates
1806
 
1807
 
1808
  def _metric_summary(payload: dict) -> dict:
1809
  keys = (
1810
+ "model_path",
1811
+ "num_samples",
1812
+ "mean_cer",
1813
+ "mean_digit_cer",
1814
+ "mean_cjk_cer",
1815
+ "mean_table_score",
1816
+ "mean_tps",
1817
+ "mean_peak_memory_mb",
1818
+ "max_tokens",
1819
+ "profile",
1820
  )
1821
  return {key: payload.get(key) for key in keys if key in payload}
1822
 
 
1836
  candidate_dir: str | Path,
1837
  reference_dir: str | Path,
1838
  source_dir: str | Path,
1839
+ calibration_dir: str | Path,
1840
  eval_dir: str | Path,
1841
  evidence_paths: dict[str, str | Path],
1842
  repo_id: str,
1843
  source_id: str,
1844
+ source_revision: str,
1845
  reference_id: str,
1846
+ reference_revision: str,
1847
  thresholds: dict | None = None,
1848
  ) -> dict:
1849
  """Build a complete release decision from on-disk evidence."""
1850
+ required = RELEASE_EVIDENCE_NAMES
1851
+ if not isinstance(evidence_paths, dict):
1852
+ raise TypeError("evidence_paths must be a dictionary")
1853
+ missing = sorted(required - set(evidence_paths))
 
 
 
1854
  if missing:
1855
  raise ValueError("Missing release evidence: " + ", ".join(missing))
1856
+ unexpected = sorted(set(evidence_paths) - required)
1857
+ if unexpected:
1858
+ raise ValueError("Unexpected release evidence: " + ", ".join(unexpected))
1859
+ evidence_files = [Path(path) for path in evidence_paths.values()]
1860
+ if len({path.name for path in evidence_files}) != len(evidence_files):
1861
+ raise ValueError("Release evidence filenames must be unique")
1862
+ symlink_evidence = [path.name for path in evidence_files if path.is_symlink()]
1863
+ if symlink_evidence:
1864
+ raise ValueError(
1865
+ "Release evidence must not be symbolic links: "
1866
+ + ", ".join(symlink_evidence)
1867
+ )
1868
+ limits = validate_release_thresholds(thresholds)
1869
+ evidence = {name: load_json_object(path) for name, path in evidence_paths.items()}
1870
 
1871
  candidate_weights = model_weight_manifest(candidate_dir)
1872
+ # Hugging Face snapshot directories use content-addressed links into the
1873
+ # immutable blob cache. Hash their resolved bytes, while keeping candidate
1874
+ # and source checkpoints link-free.
1875
+ reference_weights = model_weight_manifest(reference_dir, allow_symlinks=True)
1876
  source_weights = model_weight_manifest(source_dir)
1877
+ calibration_dataset = dataset_manifest(calibration_dir)
1878
  dataset = dataset_manifest(eval_dir)
1879
  metadata = validate_candidate_metadata(candidate_dir)
1880
+ source_config_path = Path(source_dir) / "config.json"
1881
+ if source_config_path.is_symlink() or not source_config_path.is_file():
1882
+ raise FileNotFoundError(
1883
+ f"Source config must be a regular file: {source_config_path}"
1884
+ )
1885
+ source_config_sha256 = sha256_file(source_config_path)
1886
+ candidate_files = candidate_metadata_manifest(candidate_dir)
1887
+ project_files = release_files_manifest(
1888
+ PROJECT_ROOT,
1889
+ RELEASE_PROJECT_FILES,
1890
+ required=True,
1891
+ )
1892
  gates = evaluate_release_gates(
1893
  candidate_weights=candidate_weights,
1894
  reference_weights=reference_weights,
1895
  dataset=dataset,
1896
+ calibration_dataset=calibration_dataset,
1897
  metadata=metadata,
1898
  bf16_accuracy=evidence["bf16_accuracy"],
1899
  reference_accuracy=evidence["reference_accuracy"],
 
1901
  reference_performance=evidence["reference_performance"],
1902
  candidate_performance=evidence["candidate_performance"],
1903
  rswa=evidence["candidate_rswa"],
1904
+ provenance=evidence["provenance"],
1905
+ source_model_name=Path(source_dir).name,
1906
+ source_id=source_id,
1907
+ source_config_sha256=source_config_sha256,
1908
+ source_revision=source_revision,
1909
+ reference_id=reference_id,
1910
+ reference_revision=reference_revision,
1911
+ candidate_model_name=Path(candidate_dir).name,
1912
+ repo_id=repo_id,
1913
+ thresholds=limits,
1914
+ )
1915
+ calibration_validation = validate_calibration_results(
1916
+ evidence["calibration_results"],
1917
+ bf16_accuracy=evidence["calibration_baseline_accuracy"],
1918
+ reference_performance=evidence["calibration_reference_performance"],
1919
+ calibration_dataset=calibration_dataset,
1920
+ source_revision=source_revision,
1921
+ reference_revision=reference_revision,
1922
+ evidence=evidence,
1923
+ evidence_paths=evidence_paths,
1924
+ thresholds=limits,
1925
+ )
1926
+ gates.append(
1927
+ _gate(
1928
+ "calibration_recomputed",
1929
+ calibration_validation,
1930
+ "content-addressed inputs and fastest passing experiment",
1931
+ True,
1932
+ "Calibration claims must recompute from raw benchmark evidence",
1933
+ )
1934
+ )
1935
+ sensitivity = evidence["sensitivity_results"]
1936
+ sensitivity_baseline = sensitivity.get("baseline")
1937
+ calibration_baseline = evidence["calibration_baseline_accuracy"]
1938
+ sensitivity_metric_keys = (
1939
+ "num_samples",
1940
+ "num_digit_samples",
1941
+ "num_table_samples",
1942
+ "mean_cer",
1943
+ "mean_digit_cer",
1944
+ "mean_table_score",
1945
+ )
1946
+ sensitivity_matches_calibration = (
1947
+ isinstance(sensitivity_baseline, dict)
1948
+ and sensitivity.get("dataset") == calibration_dataset
1949
+ and sensitivity.get("prompt") == calibration_baseline.get("prompt")
1950
+ and sensitivity.get("max_tokens") == calibration_baseline.get("max_tokens")
1951
+ and all(
1952
+ sensitivity_baseline.get(key) == calibration_baseline.get(key)
1953
+ for key in sensitivity_metric_keys
1954
+ )
1955
  )
1956
+ gates.append(
1957
+ _gate(
1958
+ "sensitivity_matches_calibration_dataset",
1959
+ {
1960
+ "dataset_digest": (
1961
+ sensitivity.get("dataset", {}).get("content_sha256")
1962
+ if isinstance(sensitivity.get("dataset"), dict)
1963
+ else None
1964
+ ),
1965
+ "baseline_metrics_match": sensitivity_matches_calibration,
1966
+ },
1967
+ {
1968
+ "dataset_digest": calibration_dataset.get("content_sha256"),
1969
+ "baseline_metrics_match": True,
1970
+ },
1971
+ sensitivity_matches_calibration,
1972
+ "Sensitivity decisions must use the recorded selection dataset and baseline",
1973
+ )
1974
+ )
1975
+ candidate_precision_sha = sha256_file(Path(candidate_dir) / "precision_map.json")
1976
+ evidence_precision_sha = sha256_file(evidence_paths["generated_precision_map"])
1977
+ gates.append(
1978
+ _gate(
1979
+ "candidate_precision_map_matches_evidence",
1980
+ candidate_precision_sha,
1981
+ evidence_precision_sha,
1982
+ candidate_precision_sha == evidence_precision_sha,
1983
+ "The executable candidate precision map must equal approved evidence",
1984
+ )
1985
+ )
1986
+ evidence_map = evidence["generated_precision_map"]
1987
+ map_thresholds = (
1988
+ evidence_map.get("_generated_from", {}).get("thresholds")
1989
+ if isinstance(evidence_map.get("_generated_from"), dict)
1990
+ else None
1991
+ )
1992
+ reproduce_kwargs: dict[str, Any] = {}
1993
+ if isinstance(map_thresholds, dict):
1994
+ if map_thresholds.get("cer_delta") is not None:
1995
+ reproduce_kwargs["cer_threshold"] = map_thresholds["cer_delta"]
1996
+ if map_thresholds.get("digit_cer_delta") is not None:
1997
+ reproduce_kwargs["digit_cer_threshold"] = map_thresholds[
1998
+ "digit_cer_delta"
1999
+ ]
2000
+ if map_thresholds.get("table_score_degradation") is not None:
2001
+ reproduce_kwargs["table_degradation_threshold"] = map_thresholds[
2002
+ "table_score_degradation"
2003
+ ]
2004
+ reproduced_precision_map = generate_precision_map(
2005
+ load_json_object(PROJECT_ROOT / "quantization/precision_map.json"),
2006
+ evidence["sensitivity_results"],
2007
+ calibration_results=evidence["calibration_results"],
2008
+ **reproduce_kwargs,
2009
+ )
2010
+ gates.append(
2011
+ _gate(
2012
+ "precision_map_reproducible",
2013
+ _json_digest(evidence["generated_precision_map"]),
2014
+ _json_digest(reproduced_precision_map),
2015
+ evidence["generated_precision_map"] == reproduced_precision_map,
2016
+ "Sensitivity and calibration evidence must reproduce the executable map",
2017
+ )
2018
+ )
2019
+ actual_gate_names = tuple(gate.get("name") for gate in gates)
2020
+ if actual_gate_names != MLX_RELEASE_GATE_NAMES:
2021
+ raise RuntimeError(
2022
+ "MLX release gate implementation does not match its publication contract"
2023
+ )
2024
  artifact_hashes = {
2025
  name: {
2026
  "filename": Path(path).name,
 
2029
  }
2030
  for name, path in evidence_paths.items()
2031
  }
 
2032
  return {
2033
+ "schema_version": MLX_RELEASE_SCHEMA_VERSION,
2034
  "created_at": datetime.now(timezone.utc).isoformat(),
2035
  "release_approved": all(gate["passed"] for gate in gates),
2036
  "repo_id": repo_id,
2037
+ "source": {
2038
+ "id": source_id,
2039
+ "resolved_revision": source_revision,
2040
+ "weights": source_weights,
2041
+ },
2042
+ "reference": {
2043
+ "id": reference_id,
2044
+ "resolved_revision": reference_revision,
2045
+ "weights": reference_weights,
2046
+ },
2047
  "candidate": {
2048
  "name": Path(candidate_dir).name,
2049
  "weights": candidate_weights,
2050
+ "files": candidate_files,
2051
  "metadata": metadata,
2052
  },
2053
+ "project_files": project_files,
2054
+ "calibration_dataset": calibration_dataset,
2055
+ "evaluation_dataset": dataset,
2056
  "thresholds": limits,
2057
  "metrics": {
2058
  name: _metric_summary(evidence[name])
2059
  for name in (
2060
+ "bf16_accuracy",
2061
+ "reference_accuracy",
2062
+ "candidate_accuracy",
2063
+ "reference_performance",
2064
+ "candidate_performance",
2065
  )
2066
  },
2067
  "rswa": evidence["candidate_rswa"],
 
2087
  parser.add_argument("--candidate-dir", required=True, type=Path)
2088
  parser.add_argument("--reference-dir", required=True, type=Path)
2089
  parser.add_argument("--source-dir", required=True, type=Path)
2090
+ parser.add_argument("--calibration-dir", required=True, type=Path)
2091
  parser.add_argument("--eval-dir", required=True, type=Path)
2092
  parser.add_argument("--artifacts-dir", required=True, type=Path)
2093
  parser.add_argument("--output", required=True, type=Path)
2094
  parser.add_argument("--repo-id", required=True)
2095
  parser.add_argument("--source-id", default="baidu/Unlimited-OCR")
2096
+ parser.add_argument("--source-revision", required=True)
2097
+ parser.add_argument(
2098
+ "--reference-id", default="sahilchachra/unlimited-ocr-mxfp8-mlx"
2099
+ )
2100
+ parser.add_argument("--reference-revision", required=True)
2101
  args = parser.parse_args()
2102
 
2103
  evidence_paths = {
2104
  name: args.artifacts_dir / f"{name}.json"
2105
+ for name in RELEASE_EVIDENCE_NAMES - CALIBRATION_RAW_EVIDENCE_NAMES
 
 
 
 
2106
  }
2107
+ calibration = load_json_object(evidence_paths["calibration_results"])
2108
+ evidence_paths.update(
2109
+ calibration_raw_evidence_paths(args.artifacts_dir, calibration)
2110
+ )
2111
  manifest = build_release_manifest(
2112
  candidate_dir=args.candidate_dir,
2113
  reference_dir=args.reference_dir,
2114
  source_dir=args.source_dir,
2115
+ calibration_dir=args.calibration_dir,
2116
  eval_dir=args.eval_dir,
2117
  evidence_paths=evidence_paths,
2118
  repo_id=args.repo_id,
2119
  source_id=args.source_id,
2120
+ source_revision=args.source_revision,
2121
  reference_id=args.reference_id,
2122
+ reference_revision=args.reference_revision,
2123
  )
2124
  args.output.parent.mkdir(parents=True, exist_ok=True)
2125
+ args.output.write_text(
2126
+ json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
2127
+ )
2128
  for gate in manifest["gates"]:
2129
+ print(
2130
+ f"[{'PASS' if gate['passed'] else 'FAIL'}] {gate['name']}: {gate['actual']}"
2131
+ )
2132
  print(f"Release approved: {manifest['release_approved']}")
2133
  if not manifest["release_approved"]:
2134
  raise SystemExit(1)
quantization/run_pipeline.py CHANGED
@@ -7,6 +7,7 @@ import argparse
7
  import importlib.metadata
8
  import json
9
  import platform
 
10
  import subprocess
11
  import sys
12
  import time
@@ -20,10 +21,13 @@ sys.path.insert(0, str(PROJECT_ROOT))
20
  sys.path.insert(0, str(PROJECT_ROOT / "src"))
21
 
22
  from quantization.release_gate import ( # noqa: E402
 
23
  dataset_manifest,
 
24
  generate_precision_map,
25
  load_json_object,
26
  sha256_file,
 
27
  )
28
 
29
 
@@ -72,6 +76,18 @@ def assert_unquantized_source(model_path: str) -> dict:
72
  return {"path": config_path.name, "sha256": sha256_file(config_path)}
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def system_provenance() -> dict:
76
  versions = {}
77
  for distribution in ("mlx", "mlx-vlm", "huggingface-hub", "numpy", "Pillow"):
@@ -93,7 +109,19 @@ def preflight(args: argparse.Namespace) -> bool:
93
  """Validate source, dataset, and release inputs before Metal allocation."""
94
  try:
95
  source_config = assert_unquantized_source(args.model_path)
96
- dataset = dataset_manifest(args.eval_dir)
 
 
 
 
 
 
 
 
 
 
 
 
97
  if not args.image.is_file():
98
  raise FileNotFoundError(f"Performance/R-SWA image not found: {args.image}")
99
  if args.output_dir.exists() and args.step in {"all", "convert"}:
@@ -102,11 +130,14 @@ def preflight(args: argparse.Namespace) -> bool:
102
  provenance = {
103
  **system_provenance(),
104
  "source_model": args.source_id,
 
105
  "source_local_name": Path(args.model_path).name,
106
  "source_config": source_config,
107
  "reference_model": args.reference_model,
 
108
  "target_repo": args.repo_id,
109
- "dataset": dataset,
 
110
  "smoke_image": {
111
  "name": args.image.name,
112
  "sha256": sha256_file(args.image),
@@ -124,8 +155,10 @@ def preflight(args: argparse.Namespace) -> bool:
124
  encoding="utf-8",
125
  )
126
  print(f"[OK] Unquantized source: {args.model_path}")
127
- print(f"[OK] Evaluation samples: {dataset['num_samples']}")
128
- print(f"[OK] Dataset digest: {dataset['content_sha256']}")
 
 
129
  return True
130
  except Exception as exc:
131
  print(f"[FAIL] Preflight: {exc}")
@@ -138,13 +171,171 @@ def sensitivity(args: argparse.Namespace) -> bool:
138
  str(QUANT_DIR / "layer_sensitivity.py"),
139
  "--model-path", args.model_path,
140
  "--source-id", args.source_id,
141
- "--eval-dir", str(args.eval_dir),
 
142
  "--output", str(args.artifacts_dir / "sensitivity_results.json"),
143
  "--max-tokens", str(args.accuracy_tokens),
144
  ]
145
  return run_command(cmd, "Layer sensitivity analysis", dry_run=args.dry_run)
146
 
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  def precision_map(args: argparse.Namespace) -> bool:
149
  sensitivity_path = args.artifacts_dir / "sensitivity_results.json"
150
  output_path = args.artifacts_dir / "generated_precision_map.json"
@@ -153,14 +344,39 @@ def precision_map(args: argparse.Namespace) -> bool:
153
  return True
154
  try:
155
  calibration_path = args.artifacts_dir / "calibration_results.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  generated = generate_precision_map(
157
  load_json_object(BASE_PRECISION_MAP),
158
  load_json_object(sensitivity_path),
159
- calibration_results=(
160
- load_json_object(calibration_path)
161
- if calibration_path.is_file()
162
- else None
163
- ),
164
  )
165
  output_path.write_text(
166
  json.dumps(generated, indent=2, ensure_ascii=False) + "\n",
@@ -178,14 +394,21 @@ def convert(args: argparse.Namespace) -> bool:
178
  sys.executable,
179
  str(QUANT_DIR / "mixed_precision_convert.py"),
180
  "--model-path", args.model_path,
 
181
  "--precision-map", str(args.artifacts_dir / "generated_precision_map.json"),
182
  "--output-dir", str(args.output_dir),
183
  ]
184
  return run_command(cmd, "BF16 to OCR-aware MXFP8 conversion", dry_run=args.dry_run)
185
 
186
 
187
- def _accuracy_command(model: str, output: Path, args: argparse.Namespace) -> list[str]:
188
- return [
 
 
 
 
 
 
189
  sys.executable,
190
  str(PROJECT_ROOT / "benchmarks" / "run_accuracy.py"),
191
  "--model-path", model,
@@ -194,10 +417,19 @@ def _accuracy_command(model: str, output: Path, args: argparse.Namespace) -> lis
194
  "--max-tokens", str(args.accuracy_tokens),
195
  "--profile", "accurate",
196
  ]
197
-
198
-
199
- def _performance_command(model: str, output: Path, args: argparse.Namespace) -> list[str]:
200
- return [
 
 
 
 
 
 
 
 
 
201
  sys.executable,
202
  str(PROJECT_ROOT / "benchmarks" / "run_performance.py"),
203
  "--model-path", model,
@@ -207,15 +439,18 @@ def _performance_command(model: str, output: Path, args: argparse.Namespace) ->
207
  "--warmup", str(args.performance_warmup),
208
  "--runs", str(args.performance_runs),
209
  ]
 
 
 
210
 
211
 
212
  def validate(args: argparse.Namespace) -> bool:
213
  """Benchmark BF16, Sahil reference, candidate, then stress candidate R-SWA."""
214
  jobs = [
215
- (_accuracy_command(args.model_path, args.artifacts_dir / "bf16_accuracy.json", args), "BF16 accuracy"),
216
- (_accuracy_command(args.reference_model, args.artifacts_dir / "reference_accuracy.json", args), "Sahil-reference accuracy"),
217
  (_accuracy_command(str(args.output_dir), args.artifacts_dir / "candidate_accuracy.json", args), "Candidate accuracy"),
218
- (_performance_command(args.reference_model, args.artifacts_dir / "reference_performance.json", args), "Sahil-reference performance"),
219
  (_performance_command(str(args.output_dir), args.artifacts_dir / "candidate_performance.json", args), "Candidate performance"),
220
  ([
221
  sys.executable,
@@ -230,13 +465,14 @@ def validate(args: argparse.Namespace) -> bool:
230
  return all(run_command(cmd, description, dry_run=args.dry_run) for cmd, description in jobs)
231
 
232
 
233
- def resolve_reference_dir(reference_model: str) -> Path:
234
  local = Path(reference_model)
235
  if local.is_dir():
236
  return local
237
  from huggingface_hub import snapshot_download
238
  return Path(snapshot_download(
239
  reference_model,
 
240
  allow_patterns=[
241
  "*.safetensors", "model.safetensors.index.json", "config.json",
242
  "processor_config.json", "tokenizer*.json", "special_tokens_map.json",
@@ -250,7 +486,10 @@ def gate(args: argparse.Namespace) -> bool:
250
  reference_dir = Path("<resolved-reference>")
251
  else:
252
  try:
253
- reference_dir = resolve_reference_dir(args.reference_model)
 
 
 
254
  except Exception as exc:
255
  print(f"[FAIL] Reference download: {exc}")
256
  return False
@@ -260,12 +499,15 @@ def gate(args: argparse.Namespace) -> bool:
260
  "--candidate-dir", str(args.output_dir),
261
  "--reference-dir", str(reference_dir),
262
  "--source-dir", args.model_path,
 
263
  "--eval-dir", str(args.eval_dir),
264
  "--artifacts-dir", str(args.artifacts_dir),
265
  "--output", str(args.artifacts_dir / "release_manifest.json"),
266
  "--repo-id", args.repo_id,
267
  "--source-id", args.source_id,
 
268
  "--reference-id", args.reference_model,
 
269
  ]
270
  return run_command(cmd, "Fail-closed release gate", dry_run=args.dry_run)
271
 
@@ -288,8 +530,21 @@ def parse_args() -> argparse.Namespace:
288
  parser = argparse.ArgumentParser(description="Verified OCR-aware model release pipeline")
289
  parser.add_argument("--model-path", default=str(DEFAULT_SOURCE))
290
  parser.add_argument("--source-id", default=DEFAULT_SOURCE_ID)
 
291
  parser.add_argument("--reference-model", default=DEFAULT_REFERENCE)
292
- parser.add_argument("--eval-dir", type=Path, default=PROJECT_ROOT / "eval_dataset")
 
 
 
 
 
 
 
 
 
 
 
 
293
  parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
294
  parser.add_argument("--artifacts-dir", type=Path, default=DEFAULT_ARTIFACTS)
295
  parser.add_argument("--image", type=Path, default=PROJECT_ROOT / "test_data" / "test_invoice.png")
@@ -301,17 +556,33 @@ def parse_args() -> argparse.Namespace:
301
  parser.add_argument("--rswa-lengths", type=int, nargs="+", default=[512, 2048, 8192])
302
  parser.add_argument(
303
  "--step",
304
- choices=["all", "preflight", "sensitivity", "precision-map", "convert", "validate", "gate", "publish"],
 
 
 
 
 
 
 
 
 
 
305
  default="all",
306
  )
307
  parser.add_argument("--dry-run", action="store_true")
308
  args = parser.parse_args()
309
- positive = ("accuracy_tokens", "performance_tokens", "performance_runs")
 
 
 
 
 
 
 
 
310
  for name in positive:
311
  if getattr(args, name) < 1:
312
  parser.error(f"--{name.replace('_', '-')} must be positive")
313
- if args.performance_warmup < 0:
314
- parser.error("--performance-warmup must be non-negative")
315
  if len(args.rswa_lengths) < 3 or args.rswa_lengths != sorted(set(args.rswa_lengths)) or 8192 not in args.rswa_lengths:
316
  parser.error("--rswa-lengths must be sorted, unique, include 8192, and contain at least three values")
317
  return args
@@ -322,12 +593,14 @@ def main() -> None:
322
  steps = {
323
  "preflight": lambda: preflight(args),
324
  "sensitivity": lambda: sensitivity(args),
 
325
  "precision-map": lambda: precision_map(args),
326
  "convert": lambda: convert(args),
327
  "validate": lambda: validate(args),
328
  "gate": lambda: gate(args),
329
  "publish": lambda: publish(args),
330
  }
 
331
  selected = list(steps) if args.step == "all" else [args.step]
332
  for step_name in selected:
333
  if not steps[step_name]():
 
7
  import importlib.metadata
8
  import json
9
  import platform
10
+ import re
11
  import subprocess
12
  import sys
13
  import time
 
21
  sys.path.insert(0, str(PROJECT_ROOT / "src"))
22
 
23
  from quantization.release_gate import ( # noqa: E402
24
+ calibration_raw_evidence_paths,
25
  dataset_manifest,
26
+ dataset_separation,
27
  generate_precision_map,
28
  load_json_object,
29
  sha256_file,
30
+ validate_calibration_results,
31
  )
32
 
33
 
 
76
  return {"path": config_path.name, "sha256": sha256_file(config_path)}
77
 
78
 
79
+ def verify_huggingface_revision(repo_id: str, revision: str) -> str:
80
+ """Resolve an explicitly pinned Hub revision and require an exact commit."""
81
+ from huggingface_hub import HfApi
82
+
83
+ resolved = HfApi().model_info(repo_id, revision=revision).sha
84
+ if resolved != revision:
85
+ raise ValueError(
86
+ f"Revision for {repo_id} resolved to {resolved!r}, expected {revision!r}"
87
+ )
88
+ return resolved
89
+
90
+
91
  def system_provenance() -> dict:
92
  versions = {}
93
  for distribution in ("mlx", "mlx-vlm", "huggingface-hub", "numpy", "Pillow"):
 
109
  """Validate source, dataset, and release inputs before Metal allocation."""
110
  try:
111
  source_config = assert_unquantized_source(args.model_path)
112
+ verify_huggingface_revision(args.source_id, args.source_revision)
113
+ verify_huggingface_revision(
114
+ args.reference_model,
115
+ args.reference_revision,
116
+ )
117
+ calibration_dataset = dataset_manifest(args.calibration_dir)
118
+ evaluation_dataset = dataset_manifest(args.eval_dir)
119
+ separation = dataset_separation(calibration_dataset, evaluation_dataset)
120
+ if not separation["passed"]:
121
+ raise ValueError(
122
+ "Calibration and final evaluation datasets must be disjoint; "
123
+ f"details={separation}"
124
+ )
125
  if not args.image.is_file():
126
  raise FileNotFoundError(f"Performance/R-SWA image not found: {args.image}")
127
  if args.output_dir.exists() and args.step in {"all", "convert"}:
 
130
  provenance = {
131
  **system_provenance(),
132
  "source_model": args.source_id,
133
+ "source_revision": args.source_revision,
134
  "source_local_name": Path(args.model_path).name,
135
  "source_config": source_config,
136
  "reference_model": args.reference_model,
137
+ "reference_revision": args.reference_revision,
138
  "target_repo": args.repo_id,
139
+ "calibration_dataset": calibration_dataset,
140
+ "evaluation_dataset": evaluation_dataset,
141
  "smoke_image": {
142
  "name": args.image.name,
143
  "sha256": sha256_file(args.image),
 
155
  encoding="utf-8",
156
  )
157
  print(f"[OK] Unquantized source: {args.model_path}")
158
+ print(f"[OK] Calibration samples: {calibration_dataset['num_samples']}")
159
+ print(f"[OK] Calibration digest: {calibration_dataset['content_sha256']}")
160
+ print(f"[OK] Held-out evaluation samples: {evaluation_dataset['num_samples']}")
161
+ print(f"[OK] Evaluation digest: {evaluation_dataset['content_sha256']}")
162
  return True
163
  except Exception as exc:
164
  print(f"[FAIL] Preflight: {exc}")
 
171
  str(QUANT_DIR / "layer_sensitivity.py"),
172
  "--model-path", args.model_path,
173
  "--source-id", args.source_id,
174
+ "--source-revision", args.source_revision,
175
+ "--eval-dir", str(args.calibration_dir),
176
  "--output", str(args.artifacts_dir / "sensitivity_results.json"),
177
  "--max-tokens", str(args.accuracy_tokens),
178
  ]
179
  return run_command(cmd, "Layer sensitivity analysis", dry_run=args.dry_run)
180
 
181
 
182
+ def _accuracy_on_dir(
183
+ model: str,
184
+ eval_dir: Path,
185
+ output: Path,
186
+ args: argparse.Namespace,
187
+ *,
188
+ revision: str | None = None,
189
+ ) -> list[str]:
190
+ command = [
191
+ sys.executable,
192
+ str(PROJECT_ROOT / "benchmarks" / "run_accuracy.py"),
193
+ "--model-path", model,
194
+ "--eval-dir", str(eval_dir),
195
+ "--output", str(output),
196
+ "--max-tokens", str(args.accuracy_tokens),
197
+ "--profile", "accurate",
198
+ ]
199
+ if revision is not None:
200
+ command.extend(["--served-revision", revision])
201
+ return command
202
+
203
+
204
+ def calibrate(args: argparse.Namespace) -> bool:
205
+ """Build LM-head candidates from sensitivity, measure them, select precision.
206
+
207
+ Schema-3 release requires ``calibration_results.json`` with hashed raw
208
+ evidence before the final precision map can be generated.
209
+ """
210
+ if args.dry_run:
211
+ print("[DRY RUN] Calibrate LM head (bf16 / mxfp8 / affine8 experiments)")
212
+ return True
213
+
214
+ try:
215
+ sensitivity_path = args.artifacts_dir / "sensitivity_results.json"
216
+ # Align group retention with the joint release CER/digit budgets so
217
+ # cumulative MXFP8 error is less likely to blow the 0.01 absolute caps.
218
+ interim_map = generate_precision_map(
219
+ load_json_object(BASE_PRECISION_MAP),
220
+ load_json_object(sensitivity_path),
221
+ calibration_results=None,
222
+ cer_threshold=0.01,
223
+ digit_cer_threshold=0.01,
224
+ table_degradation_threshold=0.01,
225
+ )
226
+ interim_path = args.artifacts_dir / "interim_precision_map.json"
227
+ interim_path.write_text(
228
+ json.dumps(interim_map, indent=2, ensure_ascii=False) + "\n",
229
+ encoding="utf-8",
230
+ )
231
+ print(f"[OK] Interim precision map (pre-calibration): {interim_path}")
232
+ except Exception as exc:
233
+ print(f"[FAIL] Interim precision map: {exc}")
234
+ return False
235
+
236
+ baseline_accuracy = args.artifacts_dir / "calibration_baseline_accuracy.json"
237
+ reference_performance = (
238
+ args.artifacts_dir / "calibration_reference_performance.json"
239
+ )
240
+ jobs: list[tuple[list[str], str]] = [
241
+ (
242
+ _accuracy_on_dir(
243
+ args.model_path,
244
+ args.calibration_dir,
245
+ baseline_accuracy,
246
+ args,
247
+ revision=args.source_revision,
248
+ ),
249
+ "Calibration baseline BF16 accuracy",
250
+ ),
251
+ (
252
+ _performance_command(
253
+ args.reference_model,
254
+ reference_performance,
255
+ args,
256
+ revision=args.reference_revision,
257
+ ),
258
+ "Calibration reference performance",
259
+ ),
260
+ ]
261
+ if not all(run_command(cmd, description) for cmd, description in jobs):
262
+ return False
263
+
264
+ experiment_specs = (
265
+ ("bf16-head", "bfloat16"),
266
+ ("mxfp8-head", "mxfp8"),
267
+ ("affine8-head", "affine8"),
268
+ )
269
+ experiment_args: list[str] = []
270
+ for label, precision in experiment_specs:
271
+ exp_map = dict(interim_map)
272
+ exp_map["language_model.lm_head"] = precision
273
+ exp_map_path = args.artifacts_dir / f"calibration_{precision}_precision_map.json"
274
+ exp_map_path.write_text(
275
+ json.dumps(exp_map, indent=2, ensure_ascii=False) + "\n",
276
+ encoding="utf-8",
277
+ )
278
+ exp_model_dir = (
279
+ args.output_dir.parent / f"{args.output_dir.name}-cal-{precision}"
280
+ )
281
+ if exp_model_dir.exists():
282
+ print(f"[FAIL] Calibration model directory already exists: {exp_model_dir}")
283
+ return False
284
+ convert_cmd = [
285
+ sys.executable,
286
+ str(QUANT_DIR / "mixed_precision_convert.py"),
287
+ "--model-path", args.model_path,
288
+ "--source-revision", args.source_revision,
289
+ "--precision-map", str(exp_map_path),
290
+ "--output-dir", str(exp_model_dir),
291
+ ]
292
+ if not run_command(convert_cmd, f"Calibration convert ({label})"):
293
+ return False
294
+
295
+ # Filenames must match CALIBRATION_EVIDENCE_KEYS in release_gate.py.
296
+ accuracy_path = args.artifacts_dir / f"calibration_{precision}_accuracy.json"
297
+ performance_path = (
298
+ args.artifacts_dir / f"calibration_{precision}_performance.json"
299
+ )
300
+ if not run_command(
301
+ _accuracy_on_dir(
302
+ str(exp_model_dir),
303
+ args.calibration_dir,
304
+ accuracy_path,
305
+ args,
306
+ ),
307
+ f"Calibration accuracy ({label})",
308
+ ):
309
+ return False
310
+ if not run_command(
311
+ _performance_command(str(exp_model_dir), performance_path, args),
312
+ f"Calibration performance ({label})",
313
+ ):
314
+ return False
315
+ experiment_args.extend(
316
+ [
317
+ "--experiment",
318
+ label,
319
+ precision,
320
+ str(accuracy_path),
321
+ str(performance_path),
322
+ ]
323
+ )
324
+
325
+ select_cmd = [
326
+ sys.executable,
327
+ str(QUANT_DIR / "calibrate_precision.py"),
328
+ "--bf16-accuracy", str(baseline_accuracy),
329
+ "--reference-performance", str(reference_performance),
330
+ "--calibration-dir", str(args.calibration_dir),
331
+ "--source-revision", args.source_revision,
332
+ "--reference-revision", args.reference_revision,
333
+ *experiment_args,
334
+ "--output", str(args.artifacts_dir / "calibration_results.json"),
335
+ ]
336
+ return run_command(select_cmd, "Select calibrated LM-head precision")
337
+
338
+
339
  def precision_map(args: argparse.Namespace) -> bool:
340
  sensitivity_path = args.artifacts_dir / "sensitivity_results.json"
341
  output_path = args.artifacts_dir / "generated_precision_map.json"
 
344
  return True
345
  try:
346
  calibration_path = args.artifacts_dir / "calibration_results.json"
347
+ calibration = load_json_object(calibration_path)
348
+ evidence_paths = {
349
+ "calibration_baseline_accuracy": (
350
+ args.artifacts_dir / "calibration_baseline_accuracy.json"
351
+ ),
352
+ "calibration_reference_performance": (
353
+ args.artifacts_dir / "calibration_reference_performance.json"
354
+ ),
355
+ **calibration_raw_evidence_paths(args.artifacts_dir, calibration),
356
+ }
357
+ evidence = {
358
+ name: load_json_object(path) for name, path in evidence_paths.items()
359
+ }
360
+ validate_calibration_results(
361
+ calibration,
362
+ bf16_accuracy=evidence["calibration_baseline_accuracy"],
363
+ reference_performance=evidence[
364
+ "calibration_reference_performance"
365
+ ],
366
+ calibration_dataset=dataset_manifest(args.calibration_dir),
367
+ source_revision=args.source_revision,
368
+ reference_revision=args.reference_revision,
369
+ evidence=evidence,
370
+ evidence_paths=evidence_paths,
371
+ )
372
  generated = generate_precision_map(
373
  load_json_object(BASE_PRECISION_MAP),
374
  load_json_object(sensitivity_path),
375
+ calibration_results=calibration,
376
+ # Must match the interim calibration map thresholds.
377
+ cer_threshold=0.01,
378
+ digit_cer_threshold=0.01,
379
+ table_degradation_threshold=0.01,
380
  )
381
  output_path.write_text(
382
  json.dumps(generated, indent=2, ensure_ascii=False) + "\n",
 
394
  sys.executable,
395
  str(QUANT_DIR / "mixed_precision_convert.py"),
396
  "--model-path", args.model_path,
397
+ "--source-revision", args.source_revision,
398
  "--precision-map", str(args.artifacts_dir / "generated_precision_map.json"),
399
  "--output-dir", str(args.output_dir),
400
  ]
401
  return run_command(cmd, "BF16 to OCR-aware MXFP8 conversion", dry_run=args.dry_run)
402
 
403
 
404
+ def _accuracy_command(
405
+ model: str,
406
+ output: Path,
407
+ args: argparse.Namespace,
408
+ *,
409
+ revision: str | None = None,
410
+ ) -> list[str]:
411
+ command = [
412
  sys.executable,
413
  str(PROJECT_ROOT / "benchmarks" / "run_accuracy.py"),
414
  "--model-path", model,
 
417
  "--max-tokens", str(args.accuracy_tokens),
418
  "--profile", "accurate",
419
  ]
420
+ if revision is not None:
421
+ command.extend(["--served-revision", revision])
422
+ return command
423
+
424
+
425
+ def _performance_command(
426
+ model: str,
427
+ output: Path,
428
+ args: argparse.Namespace,
429
+ *,
430
+ revision: str | None = None,
431
+ ) -> list[str]:
432
+ command = [
433
  sys.executable,
434
  str(PROJECT_ROOT / "benchmarks" / "run_performance.py"),
435
  "--model-path", model,
 
439
  "--warmup", str(args.performance_warmup),
440
  "--runs", str(args.performance_runs),
441
  ]
442
+ if revision is not None:
443
+ command.extend(["--served-revision", revision])
444
+ return command
445
 
446
 
447
  def validate(args: argparse.Namespace) -> bool:
448
  """Benchmark BF16, Sahil reference, candidate, then stress candidate R-SWA."""
449
  jobs = [
450
+ (_accuracy_command(args.model_path, args.artifacts_dir / "bf16_accuracy.json", args, revision=args.source_revision), "BF16 accuracy"),
451
+ (_accuracy_command(args.reference_model, args.artifacts_dir / "reference_accuracy.json", args, revision=args.reference_revision), "Sahil-reference accuracy"),
452
  (_accuracy_command(str(args.output_dir), args.artifacts_dir / "candidate_accuracy.json", args), "Candidate accuracy"),
453
+ (_performance_command(args.reference_model, args.artifacts_dir / "reference_performance.json", args, revision=args.reference_revision), "Sahil-reference performance"),
454
  (_performance_command(str(args.output_dir), args.artifacts_dir / "candidate_performance.json", args), "Candidate performance"),
455
  ([
456
  sys.executable,
 
465
  return all(run_command(cmd, description, dry_run=args.dry_run) for cmd, description in jobs)
466
 
467
 
468
+ def resolve_reference_dir(reference_model: str, revision: str) -> Path:
469
  local = Path(reference_model)
470
  if local.is_dir():
471
  return local
472
  from huggingface_hub import snapshot_download
473
  return Path(snapshot_download(
474
  reference_model,
475
+ revision=revision,
476
  allow_patterns=[
477
  "*.safetensors", "model.safetensors.index.json", "config.json",
478
  "processor_config.json", "tokenizer*.json", "special_tokens_map.json",
 
486
  reference_dir = Path("<resolved-reference>")
487
  else:
488
  try:
489
+ reference_dir = resolve_reference_dir(
490
+ args.reference_model,
491
+ args.reference_revision,
492
+ )
493
  except Exception as exc:
494
  print(f"[FAIL] Reference download: {exc}")
495
  return False
 
499
  "--candidate-dir", str(args.output_dir),
500
  "--reference-dir", str(reference_dir),
501
  "--source-dir", args.model_path,
502
+ "--calibration-dir", str(args.calibration_dir),
503
  "--eval-dir", str(args.eval_dir),
504
  "--artifacts-dir", str(args.artifacts_dir),
505
  "--output", str(args.artifacts_dir / "release_manifest.json"),
506
  "--repo-id", args.repo_id,
507
  "--source-id", args.source_id,
508
+ "--source-revision", args.source_revision,
509
  "--reference-id", args.reference_model,
510
+ "--reference-revision", args.reference_revision,
511
  ]
512
  return run_command(cmd, "Fail-closed release gate", dry_run=args.dry_run)
513
 
 
530
  parser = argparse.ArgumentParser(description="Verified OCR-aware model release pipeline")
531
  parser.add_argument("--model-path", default=str(DEFAULT_SOURCE))
532
  parser.add_argument("--source-id", default=DEFAULT_SOURCE_ID)
533
+ parser.add_argument("--source-revision", required=True)
534
  parser.add_argument("--reference-model", default=DEFAULT_REFERENCE)
535
+ parser.add_argument("--reference-revision", required=True)
536
+ parser.add_argument(
537
+ "--calibration-dir",
538
+ required=True,
539
+ type=Path,
540
+ help="Selection-only OCR dataset used for sensitivity and head calibration",
541
+ )
542
+ parser.add_argument(
543
+ "--eval-dir",
544
+ required=True,
545
+ type=Path,
546
+ help="Disjoint held-out OCR dataset used only for final release validation",
547
+ )
548
  parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
549
  parser.add_argument("--artifacts-dir", type=Path, default=DEFAULT_ARTIFACTS)
550
  parser.add_argument("--image", type=Path, default=PROJECT_ROOT / "test_data" / "test_invoice.png")
 
556
  parser.add_argument("--rswa-lengths", type=int, nargs="+", default=[512, 2048, 8192])
557
  parser.add_argument(
558
  "--step",
559
+ choices=[
560
+ "all",
561
+ "preflight",
562
+ "sensitivity",
563
+ "calibrate",
564
+ "precision-map",
565
+ "convert",
566
+ "validate",
567
+ "gate",
568
+ "publish",
569
+ ],
570
  default="all",
571
  )
572
  parser.add_argument("--dry-run", action="store_true")
573
  args = parser.parse_args()
574
+ for name in ("source_revision", "reference_revision"):
575
+ if not re.fullmatch(r"[0-9a-f]{40}", getattr(args, name)):
576
+ parser.error(f"--{name.replace('_', '-')} must be a 40-character lowercase commit SHA")
577
+ positive = (
578
+ "accuracy_tokens",
579
+ "performance_tokens",
580
+ "performance_warmup",
581
+ "performance_runs",
582
+ )
583
  for name in positive:
584
  if getattr(args, name) < 1:
585
  parser.error(f"--{name.replace('_', '-')} must be positive")
 
 
586
  if len(args.rswa_lengths) < 3 or args.rswa_lengths != sorted(set(args.rswa_lengths)) or 8192 not in args.rswa_lengths:
587
  parser.error("--rswa-lengths must be sorted, unique, include 8192, and contain at least three values")
588
  return args
 
593
  steps = {
594
  "preflight": lambda: preflight(args),
595
  "sensitivity": lambda: sensitivity(args),
596
+ "calibrate": lambda: calibrate(args),
597
  "precision-map": lambda: precision_map(args),
598
  "convert": lambda: convert(args),
599
  "validate": lambda: validate(args),
600
  "gate": lambda: gate(args),
601
  "publish": lambda: publish(args),
602
  }
603
+ # Full release order: selection data → calibrated map → convert → held-out gate.
604
  selected = list(steps) if args.step == "all" else [args.step]
605
  for step_name in selected:
606
  if not steps[step_name]():
quantization_config.json CHANGED
@@ -1,26 +1,27 @@
1
  {
2
  "quantization_method": "mxfp8",
3
- "description": "Block-float MX FP8 quantization (~9.19 effective bits/weight) applied to language tower linear weights. Vision encoder and embeddings remain in bfloat16.",
4
  "base_model": "baidu/Unlimited-OCR",
5
  "reference_quantization": "sahilchachra/unlimited-ocr-mxfp8-mlx",
6
  "quantized_components": {
7
  "language_model.linear": "mxfp8",
8
- "language_model.moe.experts": "mxfp8"
 
 
 
9
  },
10
  "preserved_bf16_components": {
11
  "vision_encoder": "bfloat16",
12
- "vision_projector": "bfloat16",
13
- "token_embeddings": "bfloat16",
14
  "normalization_layers": "bfloat16",
15
- "position_embeddings": "bfloat16"
16
  },
17
  "effective_bits_per_weight": 9.19,
18
  "model_size_gb": 3.83,
19
  "conversion_tool": "mlx-vlm quantizers",
20
  "notes": [
21
- "mlx-vlm quantizers only touch the language tower's linear weights",
22
- "The vision encoder and embeddings remain bf16",
23
- "On-disk size (~3.6 GB) averages both precision levels",
24
  "Published config.json uses model_type 'unlimited-ocr' to select mlx-vlm's native R-SWA implementation"
25
  ]
26
  }
 
1
  {
2
  "quantization_method": "mxfp8",
3
+ "description": "Quantization layout of the reference sahilchachra checkpoint used by the default upload workflow. MXFP8 is applied to quantizable language modules, token embeddings, the LM head, and the projector; the vision encoders and non-quantizable normalization/routing weights remain in bfloat16.",
4
  "base_model": "baidu/Unlimited-OCR",
5
  "reference_quantization": "sahilchachra/unlimited-ocr-mxfp8-mlx",
6
  "quantized_components": {
7
  "language_model.linear": "mxfp8",
8
+ "language_model.moe.experts": "mxfp8",
9
+ "language_model.token_embeddings": "mxfp8",
10
+ "language_model.lm_head": "mxfp8",
11
+ "vision_projector": "mxfp8"
12
  },
13
  "preserved_bf16_components": {
14
  "vision_encoder": "bfloat16",
 
 
15
  "normalization_layers": "bfloat16",
16
+ "moe_routing_gates": "bfloat16"
17
  },
18
  "effective_bits_per_weight": 9.19,
19
  "model_size_gb": 3.83,
20
  "conversion_tool": "mlx-vlm quantizers",
21
  "notes": [
22
+ "This file describes the reference weights copied by scripts/upload_model.py; that workflow does not run a new conversion",
23
+ "The safetensors index contains MXFP8 scale tensors for the token embeddings, LM head, and projector, but not the vision encoders",
24
+ "quantization/mixed_precision_convert.py can produce a different OCR-aware layout from the BF16 base model",
25
  "Published config.json uses model_type 'unlimited-ocr' to select mlx-vlm's native R-SWA implementation"
26
  ]
27
  }
quantization_summary.json CHANGED
@@ -2,14 +2,15 @@
2
  "method": "mxfp8",
3
  "group_size": 32,
4
  "bits": 8,
5
- "source_model": "baidu/Unlimited-OCR",
6
- "precision_map_sha256": "2c0bfbd58fcbb22e17927f3c5e8214e4933a3675a6d1a87e8b3b87b4fbb37b33",
7
- "quantized_module_count": 120,
 
8
  "quantized_precision_counts": {
9
  "affine8": 1,
10
- "mxfp8": 119
11
  },
12
- "preserved_quantizable_module_count": 145,
13
  "quantized_modules": [
14
  "language_model.lm_head",
15
  "language_model.model.embed_tokens",
@@ -129,8 +130,7 @@
129
  "language_model.model.layers.9.self_attn.k_proj",
130
  "language_model.model.layers.9.self_attn.o_proj",
131
  "language_model.model.layers.9.self_attn.q_proj",
132
- "language_model.model.layers.9.self_attn.v_proj",
133
- "projector.layers"
134
  ],
135
  "quantized_module_precisions": {
136
  "language_model.lm_head": "affine8",
@@ -251,7 +251,6 @@
251
  "language_model.model.layers.9.self_attn.k_proj": "mxfp8",
252
  "language_model.model.layers.9.self_attn.o_proj": "mxfp8",
253
  "language_model.model.layers.9.self_attn.q_proj": "mxfp8",
254
- "language_model.model.layers.9.self_attn.v_proj": "mxfp8",
255
- "projector.layers": "mxfp8"
256
  }
257
  }
 
2
  "method": "mxfp8",
3
  "group_size": 32,
4
  "bits": 8,
5
+ "source_model": "Unlimited-OCR",
6
+ "source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
7
+ "precision_map_sha256": "c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a",
8
+ "quantized_module_count": 119,
9
  "quantized_precision_counts": {
10
  "affine8": 1,
11
+ "mxfp8": 118
12
  },
13
+ "preserved_quantizable_module_count": 146,
14
  "quantized_modules": [
15
  "language_model.lm_head",
16
  "language_model.model.embed_tokens",
 
130
  "language_model.model.layers.9.self_attn.k_proj",
131
  "language_model.model.layers.9.self_attn.o_proj",
132
  "language_model.model.layers.9.self_attn.q_proj",
133
+ "language_model.model.layers.9.self_attn.v_proj"
 
134
  ],
135
  "quantized_module_precisions": {
136
  "language_model.lm_head": "affine8",
 
251
  "language_model.model.layers.9.self_attn.k_proj": "mxfp8",
252
  "language_model.model.layers.9.self_attn.o_proj": "mxfp8",
253
  "language_model.model.layers.9.self_attn.q_proj": "mxfp8",
254
+ "language_model.model.layers.9.self_attn.v_proj": "mxfp8"
 
255
  }
256
  }
release/bf16_accuracy.json CHANGED
@@ -1,5 +1,7 @@
1
  {
2
  "model_path": "Unlimited-OCR",
 
 
3
  "prompt": "<image>document parsing.",
4
  "max_tokens": 1024,
5
  "profile": "accurate",
@@ -12,183 +14,237 @@
12
  },
13
  "num_images": 12,
14
  "num_samples": 12,
15
- "total_time_seconds": 36.51101154302887,
16
- "mean_cer": 0.2391877132924964,
17
- "mean_wer": 0.5784543844272946,
18
  "num_digit_samples": 12,
19
  "num_cjk_samples": 3,
20
- "mean_digit_cer": 0.08733996757027829,
21
  "mean_cjk_cer": 0.3333333333333333,
22
  "num_table_samples": 3,
23
  "mean_table_score": 1.0,
24
  "mean_repetition_rate": 0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  "per_file": [
26
  {
27
  "file": "0001.png",
28
- "cer": 0.02315227070347284,
29
- "wer": 0.19282511210762332,
30
- "digit_cer": 0.03488372093023256,
 
31
  "cjk_cer": 0.0,
32
  "repetition_rate": 0.0,
33
  "table_score": null,
34
- "elapsed_seconds": 4.554586291997111,
35
- "ref_length": 1345,
36
- "hyp_length": 1348,
37
- "ref_digit_count": 86,
38
  "ref_cjk_count": 0
39
  },
40
  {
41
  "file": "0002.png",
42
- "cer": 0.9133034379671151,
43
- "wer": 0.46107784431137727,
44
- "digit_cer": 0.12631578947368421,
 
45
  "cjk_cer": 0.0,
46
  "repetition_rate": 0.0,
47
  "table_score": null,
48
- "elapsed_seconds": 6.1484525830019265,
49
- "ref_length": 835,
50
- "hyp_length": 1378,
51
- "ref_digit_count": 95,
52
  "ref_cjk_count": 0
53
  },
54
  {
55
  "file": "0003.png",
56
- "cer": 0.3923145665773012,
57
- "wer": 0.3317757009345794,
58
- "digit_cer": 0.18518518518518517,
 
59
  "cjk_cer": 0.0,
60
  "repetition_rate": 0.0,
61
  "table_score": null,
62
- "elapsed_seconds": 4.549903999999515,
63
- "ref_length": 1332,
64
- "hyp_length": 1719,
65
- "ref_digit_count": 27,
66
  "ref_cjk_count": 0
67
  },
68
  {
69
  "file": "0004.png",
70
- "cer": 0.37906137184115524,
71
- "wer": 0.36486486486486486,
72
- "digit_cer": 0.1016949152542373,
 
73
  "cjk_cer": 0.0,
74
  "repetition_rate": 0.0,
75
  "table_score": null,
76
- "elapsed_seconds": 4.64588491700124,
77
- "ref_length": 1329,
78
- "hyp_length": 1710,
79
- "ref_digit_count": 59,
80
  "ref_cjk_count": 0
81
  },
82
  {
83
  "file": "0005.png",
84
- "cer": 0.17715617715617715,
 
85
  "wer": 1.5,
86
- "digit_cer": 0.2,
87
  "cjk_cer": 0.0,
88
  "repetition_rate": 0.0,
89
  "table_score": 1.0,
90
- "elapsed_seconds": 3.0224843750038417,
91
- "ref_length": 436,
92
- "hyp_length": 508,
93
- "ref_digit_count": 70,
94
  "ref_cjk_count": 0
95
  },
96
  {
97
  "file": "0006.png",
98
- "cer": 0.17715617715617715,
 
99
  "wer": 1.5,
100
- "digit_cer": 0.2,
101
  "cjk_cer": 0.0,
102
  "repetition_rate": 0.0,
103
  "table_score": 1.0,
104
- "elapsed_seconds": 2.906020250011352,
105
- "ref_length": 436,
106
- "hyp_length": 508,
107
- "ref_digit_count": 70,
108
  "ref_cjk_count": 0
109
  },
110
  {
111
  "file": "0007.png",
112
- "cer": 0.17715617715617715,
 
113
  "wer": 1.5,
114
- "digit_cer": 0.2,
115
  "cjk_cer": 0.0,
116
  "repetition_rate": 0.0,
117
  "table_score": 1.0,
118
- "elapsed_seconds": 2.9044155000010505,
119
- "ref_length": 436,
120
- "hyp_length": 508,
121
- "ref_digit_count": 70,
122
  "ref_cjk_count": 0
123
  },
124
  {
125
  "file": "0008.png",
 
126
  "cer": 0.0,
127
  "wer": 0.0,
128
  "digit_cer": 0.0,
129
  "cjk_cer": 0.0,
130
  "repetition_rate": 0.0,
131
  "table_score": null,
132
- "elapsed_seconds": 1.5957032919977792,
133
- "ref_length": 92,
134
- "hyp_length": 92,
135
- "ref_digit_count": 26,
136
  "ref_cjk_count": 41
137
  },
138
  {
139
  "file": "0009.png",
 
140
  "cer": 0.0,
141
  "wer": 0.0,
142
  "digit_cer": 0.0,
143
  "cjk_cer": 0.0,
144
  "repetition_rate": 0.0,
145
  "table_score": null,
146
- "elapsed_seconds": 1.5902405830129283,
147
- "ref_length": 93,
148
- "hyp_length": 93,
149
- "ref_digit_count": 25,
150
  "ref_cjk_count": 42
151
  },
152
  {
153
  "file": "0010.png",
154
- "cer": 0.6309523809523809,
155
- "wer": 1.0909090909090908,
 
156
  "digit_cer": 0.0,
157
  "cjk_cer": 1.0,
158
  "repetition_rate": 0.0,
159
  "table_score": null,
160
- "elapsed_seconds": 1.6077932920015883,
161
- "ref_length": 105,
162
- "hyp_length": 117,
163
- "ref_digit_count": 25,
164
  "ref_cjk_count": 44
165
  },
166
  {
167
  "file": "0011.png",
 
168
  "cer": 0.0,
169
  "wer": 0.0,
170
  "digit_cer": 0.0,
171
  "cjk_cer": 0.0,
172
  "repetition_rate": 0.0,
173
  "table_score": null,
174
- "elapsed_seconds": 1.4870185839972692,
175
- "ref_length": 118,
176
- "hyp_length": 118,
177
- "ref_digit_count": 27,
178
  "ref_cjk_count": 0
179
  },
180
  {
181
  "file": "0012.png",
 
182
  "cer": 0.0,
183
  "wer": 0.0,
184
  "digit_cer": 0.0,
185
  "cjk_cer": 0.0,
186
  "repetition_rate": 0.0,
187
  "table_score": null,
188
- "elapsed_seconds": 1.4985078750032699,
189
- "ref_length": 118,
190
- "hyp_length": 118,
191
- "ref_digit_count": 27,
192
  "ref_cjk_count": 0
193
  }
194
  ]
 
1
  {
2
  "model_path": "Unlimited-OCR",
3
+ "served_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
4
+ "backend": "mlx",
5
  "prompt": "<image>document parsing.",
6
  "max_tokens": 1024,
7
  "profile": "accurate",
 
14
  },
15
  "num_images": 12,
16
  "num_samples": 12,
17
+ "total_time_seconds": 169.77583128899278,
18
+ "mean_cer": 0.205212265458972,
19
+ "mean_wer": 0.5919915970344224,
20
  "num_digit_samples": 12,
21
  "num_cjk_samples": 3,
22
+ "mean_digit_cer": 0.11330969496189873,
23
  "mean_cjk_cer": 0.3333333333333333,
24
  "num_table_samples": 3,
25
  "mean_table_score": 1.0,
26
  "mean_repetition_rate": 0.0,
27
+ "by_category": {
28
+ "cjk_document": {
29
+ "num_samples": 3,
30
+ "mean_cer": 0.18681318681318682,
31
+ "mean_digit_cer": 0.0,
32
+ "mean_cjk_cer": 0.3333333333333333,
33
+ "mean_table_score": null,
34
+ "num_digit_samples": 3,
35
+ "num_cjk_samples": 3,
36
+ "num_table_samples": 0
37
+ },
38
+ "english_document": {
39
+ "num_samples": 4,
40
+ "mean_cer": 0.34397793132397353,
41
+ "mean_digit_cer": 0.1993040848856962,
42
+ "mean_cjk_cer": null,
43
+ "mean_table_score": null,
44
+ "num_digit_samples": 4,
45
+ "num_cjk_samples": 0,
46
+ "num_table_samples": 0
47
+ },
48
+ "low_quality_scan": {
49
+ "num_samples": 2,
50
+ "mean_cer": 0.0,
51
+ "mean_digit_cer": 0.0,
52
+ "mean_cjk_cer": null,
53
+ "mean_table_score": null,
54
+ "num_digit_samples": 2,
55
+ "num_cjk_samples": 0,
56
+ "num_table_samples": 0
57
+ },
58
+ "table_numeric": {
59
+ "num_samples": 3,
60
+ "mean_cer": 0.1753986332574032,
61
+ "mean_digit_cer": 0.1875,
62
+ "mean_cjk_cer": null,
63
+ "mean_table_score": 1.0,
64
+ "num_digit_samples": 3,
65
+ "num_cjk_samples": 0,
66
+ "num_table_samples": 3
67
+ }
68
+ },
69
  "per_file": [
70
  {
71
  "file": "0001.png",
72
+ "category": "english_document",
73
+ "cer": 0.3923145665773012,
74
+ "wer": 0.3317757009345794,
75
+ "digit_cer": 0.18518518518518517,
76
  "cjk_cer": 0.0,
77
  "repetition_rate": 0.0,
78
  "table_score": null,
79
+ "elapsed_seconds": 25.946757499987143,
80
+ "ref_length": 1332,
81
+ "hyp_length": 1719,
82
+ "ref_digit_count": 27,
83
  "ref_cjk_count": 0
84
  },
85
  {
86
  "file": "0002.png",
87
+ "category": "english_document",
88
+ "cer": 0.37906137184115524,
89
+ "wer": 0.36486486486486486,
90
+ "digit_cer": 0.1016949152542373,
91
  "cjk_cer": 0.0,
92
  "repetition_rate": 0.0,
93
  "table_score": null,
94
+ "elapsed_seconds": 27.539474832999986,
95
+ "ref_length": 1329,
96
+ "hyp_length": 1710,
97
+ "ref_digit_count": 59,
98
  "ref_cjk_count": 0
99
  },
100
  {
101
  "file": "0003.png",
102
+ "category": "english_document",
103
+ "cer": 0.525911708253359,
104
+ "wer": 0.46551724137931033,
105
+ "digit_cer": 0.273972602739726,
106
  "cjk_cer": 0.0,
107
  "repetition_rate": 0.0,
108
  "table_score": null,
109
+ "elapsed_seconds": 28.937275707998197,
110
+ "ref_length": 636,
111
+ "hyp_length": 824,
112
+ "ref_digit_count": 73,
113
  "ref_cjk_count": 0
114
  },
115
  {
116
  "file": "0004.png",
117
+ "category": "english_document",
118
+ "cer": 0.07862407862407862,
119
+ "wer": 0.16901408450704225,
120
+ "digit_cer": 0.23636363636363636,
121
  "cjk_cer": 0.0,
122
  "repetition_rate": 0.0,
123
  "table_score": null,
124
+ "elapsed_seconds": 14.294532250001794,
125
+ "ref_length": 477,
126
+ "hyp_length": 476,
127
+ "ref_digit_count": 55,
128
  "ref_cjk_count": 0
129
  },
130
  {
131
  "file": "0005.png",
132
+ "category": "table_numeric",
133
+ "cer": 0.17539863325740318,
134
  "wer": 1.5,
135
+ "digit_cer": 0.1875,
136
  "cjk_cer": 0.0,
137
  "repetition_rate": 0.0,
138
  "table_score": 1.0,
139
+ "elapsed_seconds": 12.845850040990626,
140
+ "ref_length": 446,
141
+ "hyp_length": 519,
142
+ "ref_digit_count": 80,
143
  "ref_cjk_count": 0
144
  },
145
  {
146
  "file": "0006.png",
147
+ "category": "table_numeric",
148
+ "cer": 0.17539863325740318,
149
  "wer": 1.5,
150
+ "digit_cer": 0.1875,
151
  "cjk_cer": 0.0,
152
  "repetition_rate": 0.0,
153
  "table_score": 1.0,
154
+ "elapsed_seconds": 12.608022582993726,
155
+ "ref_length": 446,
156
+ "hyp_length": 519,
157
+ "ref_digit_count": 80,
158
  "ref_cjk_count": 0
159
  },
160
  {
161
  "file": "0007.png",
162
+ "category": "table_numeric",
163
+ "cer": 0.17539863325740318,
164
  "wer": 1.5,
165
+ "digit_cer": 0.1875,
166
  "cjk_cer": 0.0,
167
  "repetition_rate": 0.0,
168
  "table_score": 1.0,
169
+ "elapsed_seconds": 12.634055832997547,
170
+ "ref_length": 446,
171
+ "hyp_length": 519,
172
+ "ref_digit_count": 80,
173
  "ref_cjk_count": 0
174
  },
175
  {
176
  "file": "0008.png",
177
+ "category": "cjk_document",
178
  "cer": 0.0,
179
  "wer": 0.0,
180
  "digit_cer": 0.0,
181
  "cjk_cer": 0.0,
182
  "repetition_rate": 0.0,
183
  "table_score": null,
184
+ "elapsed_seconds": 7.23521095799515,
185
+ "ref_length": 98,
186
+ "hyp_length": 98,
187
+ "ref_digit_count": 30,
188
  "ref_cjk_count": 41
189
  },
190
  {
191
  "file": "0009.png",
192
+ "category": "cjk_document",
193
  "cer": 0.0,
194
  "wer": 0.0,
195
  "digit_cer": 0.0,
196
  "cjk_cer": 0.0,
197
  "repetition_rate": 0.0,
198
  "table_score": null,
199
+ "elapsed_seconds": 7.373159375012619,
200
+ "ref_length": 100,
201
+ "hyp_length": 100,
202
+ "ref_digit_count": 30,
203
  "ref_cjk_count": 42
204
  },
205
  {
206
  "file": "0010.png",
207
+ "category": "cjk_document",
208
+ "cer": 0.5604395604395604,
209
+ "wer": 1.2727272727272727,
210
  "digit_cer": 0.0,
211
  "cjk_cer": 1.0,
212
  "repetition_rate": 0.0,
213
  "table_score": null,
214
+ "elapsed_seconds": 7.378904458004399,
215
+ "ref_length": 112,
216
+ "hyp_length": 126,
217
+ "ref_digit_count": 30,
218
  "ref_cjk_count": 44
219
  },
220
  {
221
  "file": "0011.png",
222
+ "category": "low_quality_scan",
223
  "cer": 0.0,
224
  "wer": 0.0,
225
  "digit_cer": 0.0,
226
  "cjk_cer": 0.0,
227
  "repetition_rate": 0.0,
228
  "table_score": null,
229
+ "elapsed_seconds": 6.398878874999355,
230
+ "ref_length": 120,
231
+ "hyp_length": 120,
232
+ "ref_digit_count": 29,
233
  "ref_cjk_count": 0
234
  },
235
  {
236
  "file": "0012.png",
237
+ "category": "low_quality_scan",
238
  "cer": 0.0,
239
  "wer": 0.0,
240
  "digit_cer": 0.0,
241
  "cjk_cer": 0.0,
242
  "repetition_rate": 0.0,
243
  "table_score": null,
244
+ "elapsed_seconds": 6.583708875012235,
245
+ "ref_length": 120,
246
+ "hyp_length": 120,
247
+ "ref_digit_count": 29,
248
  "ref_cjk_count": 0
249
  }
250
  ]
release/calibration_affine8_accuracy.json ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8",
3
+ "served_revision": null,
4
+ "backend": "mlx",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 1024,
7
+ "profile": "accurate",
8
+ "generation_settings": {
9
+ "temperature": 0.0,
10
+ "top_p": 1.0,
11
+ "repetition_penalty": 1.0,
12
+ "no_repeat_ngram_size": 35,
13
+ "ngram_window": 128
14
+ },
15
+ "num_images": 10,
16
+ "num_samples": 10,
17
+ "total_time_seconds": 121.92750062300183,
18
+ "mean_cer": 0.19105830313811087,
19
+ "mean_wer": 0.647581477892157,
20
+ "num_digit_samples": 10,
21
+ "num_cjk_samples": 3,
22
+ "mean_digit_cer": 0.07857558139534884,
23
+ "mean_cjk_cer": 0.3333333333333333,
24
+ "num_table_samples": 3,
25
+ "mean_table_score": 1.0,
26
+ "mean_repetition_rate": 0.0,
27
+ "by_category": {
28
+ "cjk_document": {
29
+ "num_samples": 3,
30
+ "mean_cer": 0.18315018315018317,
31
+ "mean_digit_cer": 0.0,
32
+ "mean_cjk_cer": 0.3333333333333333,
33
+ "mean_table_score": null,
34
+ "num_digit_samples": 3,
35
+ "num_cjk_samples": 3,
36
+ "num_table_samples": 0
37
+ },
38
+ "english_document": {
39
+ "num_samples": 2,
40
+ "mean_cer": 0.4174682910791748,
41
+ "mean_digit_cer": 0.11162790697674418,
42
+ "mean_cjk_cer": null,
43
+ "mean_table_score": null,
44
+ "num_digit_samples": 2,
45
+ "num_cjk_samples": 0,
46
+ "num_table_samples": 0
47
+ },
48
+ "low_quality_scan": {
49
+ "num_samples": 2,
50
+ "mean_cer": 0.0,
51
+ "mean_digit_cer": 0.0,
52
+ "mean_cjk_cer": null,
53
+ "mean_table_score": null,
54
+ "num_digit_samples": 2,
55
+ "num_cjk_samples": 0,
56
+ "num_table_samples": 0
57
+ },
58
+ "table_numeric": {
59
+ "num_samples": 3,
60
+ "mean_cer": 0.1753986332574032,
61
+ "mean_digit_cer": 0.1875,
62
+ "mean_cjk_cer": null,
63
+ "mean_table_score": 1.0,
64
+ "num_digit_samples": 3,
65
+ "num_cjk_samples": 0,
66
+ "num_table_samples": 3
67
+ }
68
+ },
69
+ "per_file": [
70
+ {
71
+ "file": "0001.png",
72
+ "category": "english_document",
73
+ "cer": 0.08904719501335707,
74
+ "wer": 0.24663677130044842,
75
+ "digit_cer": 0.023255813953488372,
76
+ "cjk_cer": 0.0,
77
+ "repetition_rate": 0.0,
78
+ "table_score": null,
79
+ "elapsed_seconds": 22.065639082997222,
80
+ "ref_length": 1345,
81
+ "hyp_length": 1259,
82
+ "ref_digit_count": 86,
83
+ "ref_cjk_count": 0
84
+ },
85
+ {
86
+ "file": "0002.png",
87
+ "category": "english_document",
88
+ "cer": 0.7458893871449925,
89
+ "wer": 0.592814371257485,
90
+ "digit_cer": 0.2,
91
+ "cjk_cer": 0.0,
92
+ "repetition_rate": 0.0,
93
+ "table_score": null,
94
+ "elapsed_seconds": 31.2265478749905,
95
+ "ref_length": 835,
96
+ "hyp_length": 1275,
97
+ "ref_digit_count": 95,
98
+ "ref_cjk_count": 0
99
+ },
100
+ {
101
+ "file": "0003.png",
102
+ "category": "table_numeric",
103
+ "cer": 0.17539863325740318,
104
+ "wer": 1.5,
105
+ "digit_cer": 0.1875,
106
+ "cjk_cer": 0.0,
107
+ "repetition_rate": 0.0,
108
+ "table_score": 1.0,
109
+ "elapsed_seconds": 11.632652583008166,
110
+ "ref_length": 446,
111
+ "hyp_length": 519,
112
+ "ref_digit_count": 80,
113
+ "ref_cjk_count": 0
114
+ },
115
+ {
116
+ "file": "0004.png",
117
+ "category": "table_numeric",
118
+ "cer": 0.17539863325740318,
119
+ "wer": 1.5,
120
+ "digit_cer": 0.1875,
121
+ "cjk_cer": 0.0,
122
+ "repetition_rate": 0.0,
123
+ "table_score": 1.0,
124
+ "elapsed_seconds": 11.664347833007923,
125
+ "ref_length": 446,
126
+ "hyp_length": 519,
127
+ "ref_digit_count": 80,
128
+ "ref_cjk_count": 0
129
+ },
130
+ {
131
+ "file": "0005.png",
132
+ "category": "table_numeric",
133
+ "cer": 0.17539863325740318,
134
+ "wer": 1.5,
135
+ "digit_cer": 0.1875,
136
+ "cjk_cer": 0.0,
137
+ "repetition_rate": 0.0,
138
+ "table_score": 1.0,
139
+ "elapsed_seconds": 11.887310291000176,
140
+ "ref_length": 446,
141
+ "hyp_length": 519,
142
+ "ref_digit_count": 80,
143
+ "ref_cjk_count": 0
144
+ },
145
+ {
146
+ "file": "0006.png",
147
+ "category": "cjk_document",
148
+ "cer": 0.0,
149
+ "wer": 0.0,
150
+ "digit_cer": 0.0,
151
+ "cjk_cer": 0.0,
152
+ "repetition_rate": 0.0,
153
+ "table_score": null,
154
+ "elapsed_seconds": 6.9882367919926764,
155
+ "ref_length": 98,
156
+ "hyp_length": 98,
157
+ "ref_digit_count": 30,
158
+ "ref_cjk_count": 41
159
+ },
160
+ {
161
+ "file": "0007.png",
162
+ "category": "cjk_document",
163
+ "cer": 0.0,
164
+ "wer": 0.0,
165
+ "digit_cer": 0.0,
166
+ "cjk_cer": 0.0,
167
+ "repetition_rate": 0.0,
168
+ "table_score": null,
169
+ "elapsed_seconds": 6.911115791008342,
170
+ "ref_length": 100,
171
+ "hyp_length": 100,
172
+ "ref_digit_count": 30,
173
+ "ref_cjk_count": 42
174
+ },
175
+ {
176
+ "file": "0008.png",
177
+ "category": "cjk_document",
178
+ "cer": 0.5494505494505495,
179
+ "wer": 1.1363636363636365,
180
+ "digit_cer": 0.0,
181
+ "cjk_cer": 1.0,
182
+ "repetition_rate": 0.0,
183
+ "table_score": null,
184
+ "elapsed_seconds": 7.001193958989461,
185
+ "ref_length": 112,
186
+ "hyp_length": 120,
187
+ "ref_digit_count": 30,
188
+ "ref_cjk_count": 44
189
+ },
190
+ {
191
+ "file": "0009.png",
192
+ "category": "low_quality_scan",
193
+ "cer": 0.0,
194
+ "wer": 0.0,
195
+ "digit_cer": 0.0,
196
+ "cjk_cer": 0.0,
197
+ "repetition_rate": 0.0,
198
+ "table_score": null,
199
+ "elapsed_seconds": 6.5204695830034325,
200
+ "ref_length": 120,
201
+ "hyp_length": 120,
202
+ "ref_digit_count": 29,
203
+ "ref_cjk_count": 0
204
+ },
205
+ {
206
+ "file": "0010.png",
207
+ "category": "low_quality_scan",
208
+ "cer": 0.0,
209
+ "wer": 0.0,
210
+ "digit_cer": 0.0,
211
+ "cjk_cer": 0.0,
212
+ "repetition_rate": 0.0,
213
+ "table_score": null,
214
+ "elapsed_seconds": 6.029986833003932,
215
+ "ref_length": 120,
216
+ "hyp_length": 120,
217
+ "ref_digit_count": 29,
218
+ "ref_cjk_count": 0
219
+ }
220
+ ]
221
+ }
release/calibration_affine8_performance.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8",
3
+ "served_revision": null,
4
+ "image_path": "test_invoice.png",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 256,
7
+ "system": {
8
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
9
+ "processor": "arm",
10
+ "python_version": "3.14.6",
11
+ "machine": "arm64",
12
+ "mlx_version": "0.32.0",
13
+ "mlx_vlm_version": "0.6.6",
14
+ "chip": "Apple M3 Max",
15
+ "total_memory_gb": 128.0
16
+ },
17
+ "model_load_time_seconds": 1.0313333749945741,
18
+ "memory_after_load_mb": 4098.546875,
19
+ "num_warmup": 1,
20
+ "num_runs": 3,
21
+ "mean_tps": 39.58614991640946,
22
+ "std_tps": 0.7627654762339389,
23
+ "mean_elapsed_seconds": 8.051595569670704,
24
+ "mean_peak_memory_mb": 5275.928047,
25
+ "runs": [
26
+ {
27
+ "run": 1,
28
+ "elapsed_seconds": 7.985530750011094,
29
+ "tokens_generated": 256,
30
+ "tokens_generated_source": "mlx-vlm token count",
31
+ "tokens_per_second": 40.03117010793416,
32
+ "tokens_per_second_source": "mlx-vlm generation_tps",
33
+ "peak_memory_mb": 5275.752407,
34
+ "memory_delta_mb": 1337.1867550000002,
35
+ "process_peak_rss_mb": 4120.59375,
36
+ "prompt_tokens": 697,
37
+ "prompt_tokens_per_second": 453.0235706986471,
38
+ "finish_reason": "length"
39
+ },
40
+ {
41
+ "run": 2,
42
+ "elapsed_seconds": 7.846619042000384,
43
+ "tokens_generated": 256,
44
+ "tokens_generated_source": "mlx-vlm token count",
45
+ "tokens_per_second": 40.21463014341998,
46
+ "tokens_per_second_source": "mlx-vlm generation_tps",
47
+ "peak_memory_mb": 5276.015867,
48
+ "memory_delta_mb": 1337.188071,
49
+ "process_peak_rss_mb": 4121.078125,
50
+ "prompt_tokens": 697,
51
+ "prompt_tokens_per_second": 487.66087944129447,
52
+ "finish_reason": "length"
53
+ },
54
+ {
55
+ "run": 3,
56
+ "elapsed_seconds": 8.322636917000636,
57
+ "tokens_generated": 256,
58
+ "tokens_generated_source": "mlx-vlm token count",
59
+ "tokens_per_second": 38.51264949787424,
60
+ "tokens_per_second_source": "mlx-vlm generation_tps",
61
+ "peak_memory_mb": 5276.015867,
62
+ "memory_delta_mb": 1337.188071,
63
+ "process_peak_rss_mb": 4121.21875,
64
+ "prompt_tokens": 697,
65
+ "prompt_tokens_per_second": 423.58096065031117,
66
+ "finish_reason": "length"
67
+ }
68
+ ]
69
+ }
release/calibration_baseline_accuracy.json ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "Unlimited-OCR",
3
+ "served_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
4
+ "backend": "mlx",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 1024,
7
+ "profile": "accurate",
8
+ "generation_settings": {
9
+ "temperature": 0.0,
10
+ "top_p": 1.0,
11
+ "repetition_penalty": 1.0,
12
+ "no_repeat_ngram_size": 35,
13
+ "ngram_window": 128
14
+ },
15
+ "num_images": 10,
16
+ "num_samples": 10,
17
+ "total_time_seconds": 121.70595645793946,
18
+ "mean_cer": 0.20230911688823577,
19
+ "mean_wer": 0.6426630229146273,
20
+ "num_digit_samples": 10,
21
+ "num_cjk_samples": 3,
22
+ "mean_digit_cer": 0.07236995104039168,
23
+ "mean_cjk_cer": 0.3333333333333333,
24
+ "num_table_samples": 3,
25
+ "mean_table_score": 1.0,
26
+ "mean_repetition_rate": 0.0,
27
+ "by_category": {
28
+ "cjk_document": {
29
+ "num_samples": 3,
30
+ "mean_cer": 0.18681318681318682,
31
+ "mean_digit_cer": 0.0,
32
+ "mean_cjk_cer": 0.3333333333333333,
33
+ "mean_table_score": null,
34
+ "num_digit_samples": 3,
35
+ "num_cjk_samples": 3,
36
+ "num_table_samples": 0
37
+ },
38
+ "english_document": {
39
+ "num_samples": 2,
40
+ "mean_cer": 0.46822785433529396,
41
+ "mean_digit_cer": 0.08059975520195839,
42
+ "mean_cjk_cer": null,
43
+ "mean_table_score": null,
44
+ "num_digit_samples": 2,
45
+ "num_cjk_samples": 0,
46
+ "num_table_samples": 0
47
+ },
48
+ "low_quality_scan": {
49
+ "num_samples": 2,
50
+ "mean_cer": 0.0,
51
+ "mean_digit_cer": 0.0,
52
+ "mean_cjk_cer": null,
53
+ "mean_table_score": null,
54
+ "num_digit_samples": 2,
55
+ "num_cjk_samples": 0,
56
+ "num_table_samples": 0
57
+ },
58
+ "table_numeric": {
59
+ "num_samples": 3,
60
+ "mean_cer": 0.1753986332574032,
61
+ "mean_digit_cer": 0.1875,
62
+ "mean_cjk_cer": null,
63
+ "mean_table_score": 1.0,
64
+ "num_digit_samples": 3,
65
+ "num_cjk_samples": 0,
66
+ "num_table_samples": 3
67
+ }
68
+ },
69
+ "per_file": [
70
+ {
71
+ "file": "0001.png",
72
+ "category": "english_document",
73
+ "cer": 0.02315227070347284,
74
+ "wer": 0.19282511210762332,
75
+ "digit_cer": 0.03488372093023256,
76
+ "cjk_cer": 0.0,
77
+ "repetition_rate": 0.0,
78
+ "table_score": null,
79
+ "elapsed_seconds": 23.124094749990036,
80
+ "ref_length": 1345,
81
+ "hyp_length": 1348,
82
+ "ref_digit_count": 86,
83
+ "ref_cjk_count": 0
84
+ },
85
+ {
86
+ "file": "0002.png",
87
+ "category": "english_document",
88
+ "cer": 0.9133034379671151,
89
+ "wer": 0.46107784431137727,
90
+ "digit_cer": 0.12631578947368421,
91
+ "cjk_cer": 0.0,
92
+ "repetition_rate": 0.0,
93
+ "table_score": null,
94
+ "elapsed_seconds": 31.42361595899274,
95
+ "ref_length": 835,
96
+ "hyp_length": 1378,
97
+ "ref_digit_count": 95,
98
+ "ref_cjk_count": 0
99
+ },
100
+ {
101
+ "file": "0003.png",
102
+ "category": "table_numeric",
103
+ "cer": 0.17539863325740318,
104
+ "wer": 1.5,
105
+ "digit_cer": 0.1875,
106
+ "cjk_cer": 0.0,
107
+ "repetition_rate": 0.0,
108
+ "table_score": 1.0,
109
+ "elapsed_seconds": 11.507403457988403,
110
+ "ref_length": 446,
111
+ "hyp_length": 519,
112
+ "ref_digit_count": 80,
113
+ "ref_cjk_count": 0
114
+ },
115
+ {
116
+ "file": "0004.png",
117
+ "category": "table_numeric",
118
+ "cer": 0.17539863325740318,
119
+ "wer": 1.5,
120
+ "digit_cer": 0.1875,
121
+ "cjk_cer": 0.0,
122
+ "repetition_rate": 0.0,
123
+ "table_score": 1.0,
124
+ "elapsed_seconds": 11.631738583993865,
125
+ "ref_length": 446,
126
+ "hyp_length": 519,
127
+ "ref_digit_count": 80,
128
+ "ref_cjk_count": 0
129
+ },
130
+ {
131
+ "file": "0005.png",
132
+ "category": "table_numeric",
133
+ "cer": 0.17539863325740318,
134
+ "wer": 1.5,
135
+ "digit_cer": 0.1875,
136
+ "cjk_cer": 0.0,
137
+ "repetition_rate": 0.0,
138
+ "table_score": 1.0,
139
+ "elapsed_seconds": 11.604318874989985,
140
+ "ref_length": 446,
141
+ "hyp_length": 519,
142
+ "ref_digit_count": 80,
143
+ "ref_cjk_count": 0
144
+ },
145
+ {
146
+ "file": "0006.png",
147
+ "category": "cjk_document",
148
+ "cer": 0.0,
149
+ "wer": 0.0,
150
+ "digit_cer": 0.0,
151
+ "cjk_cer": 0.0,
152
+ "repetition_rate": 0.0,
153
+ "table_score": null,
154
+ "elapsed_seconds": 6.63977562499349,
155
+ "ref_length": 98,
156
+ "hyp_length": 98,
157
+ "ref_digit_count": 30,
158
+ "ref_cjk_count": 41
159
+ },
160
+ {
161
+ "file": "0007.png",
162
+ "category": "cjk_document",
163
+ "cer": 0.0,
164
+ "wer": 0.0,
165
+ "digit_cer": 0.0,
166
+ "cjk_cer": 0.0,
167
+ "repetition_rate": 0.0,
168
+ "table_score": null,
169
+ "elapsed_seconds": 6.620961624998017,
170
+ "ref_length": 100,
171
+ "hyp_length": 100,
172
+ "ref_digit_count": 30,
173
+ "ref_cjk_count": 42
174
+ },
175
+ {
176
+ "file": "0008.png",
177
+ "category": "cjk_document",
178
+ "cer": 0.5604395604395604,
179
+ "wer": 1.2727272727272727,
180
+ "digit_cer": 0.0,
181
+ "cjk_cer": 1.0,
182
+ "repetition_rate": 0.0,
183
+ "table_score": null,
184
+ "elapsed_seconds": 6.897685666001053,
185
+ "ref_length": 112,
186
+ "hyp_length": 126,
187
+ "ref_digit_count": 30,
188
+ "ref_cjk_count": 44
189
+ },
190
+ {
191
+ "file": "0009.png",
192
+ "category": "low_quality_scan",
193
+ "cer": 0.0,
194
+ "wer": 0.0,
195
+ "digit_cer": 0.0,
196
+ "cjk_cer": 0.0,
197
+ "repetition_rate": 0.0,
198
+ "table_score": null,
199
+ "elapsed_seconds": 6.136604707993683,
200
+ "ref_length": 120,
201
+ "hyp_length": 120,
202
+ "ref_digit_count": 29,
203
+ "ref_cjk_count": 0
204
+ },
205
+ {
206
+ "file": "0010.png",
207
+ "category": "low_quality_scan",
208
+ "cer": 0.0,
209
+ "wer": 0.0,
210
+ "digit_cer": 0.0,
211
+ "cjk_cer": 0.0,
212
+ "repetition_rate": 0.0,
213
+ "table_score": null,
214
+ "elapsed_seconds": 6.119757207998191,
215
+ "ref_length": 120,
216
+ "hyp_length": 120,
217
+ "ref_digit_count": 29,
218
+ "ref_cjk_count": 0
219
+ }
220
+ ]
221
+ }
release/calibration_bfloat16_accuracy.json ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16",
3
+ "served_revision": null,
4
+ "backend": "mlx",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 1024,
7
+ "profile": "accurate",
8
+ "generation_settings": {
9
+ "temperature": 0.0,
10
+ "top_p": 1.0,
11
+ "repetition_penalty": 1.0,
12
+ "no_repeat_ngram_size": 35,
13
+ "ngram_window": 128
14
+ },
15
+ "num_images": 10,
16
+ "num_samples": 10,
17
+ "total_time_seconds": 130.78621862299042,
18
+ "mean_cer": 0.19010066585760363,
19
+ "mean_wer": 0.6393071175219882,
20
+ "num_digit_samples": 10,
21
+ "num_cjk_samples": 3,
22
+ "mean_digit_cer": 0.06278610771113831,
23
+ "mean_cjk_cer": 0.3333333333333333,
24
+ "num_table_samples": 3,
25
+ "mean_table_score": 1.0,
26
+ "mean_repetition_rate": 0.0,
27
+ "by_category": {
28
+ "cjk_document": {
29
+ "num_samples": 3,
30
+ "mean_cer": 0.16849816849816848,
31
+ "mean_digit_cer": 0.0,
32
+ "mean_cjk_cer": 0.3333333333333333,
33
+ "mean_table_score": null,
34
+ "num_digit_samples": 3,
35
+ "num_cjk_samples": 3,
36
+ "num_table_samples": 0
37
+ },
38
+ "english_document": {
39
+ "num_samples": 2,
40
+ "mean_cer": 0.43465812665466064,
41
+ "mean_digit_cer": 0.03268053855569156,
42
+ "mean_cjk_cer": null,
43
+ "mean_table_score": null,
44
+ "num_digit_samples": 2,
45
+ "num_cjk_samples": 0,
46
+ "num_table_samples": 0
47
+ },
48
+ "low_quality_scan": {
49
+ "num_samples": 2,
50
+ "mean_cer": 0.0,
51
+ "mean_digit_cer": 0.0,
52
+ "mean_cjk_cer": null,
53
+ "mean_table_score": null,
54
+ "num_digit_samples": 2,
55
+ "num_cjk_samples": 0,
56
+ "num_table_samples": 0
57
+ },
58
+ "table_numeric": {
59
+ "num_samples": 3,
60
+ "mean_cer": 0.1753986332574032,
61
+ "mean_digit_cer": 0.1875,
62
+ "mean_cjk_cer": null,
63
+ "mean_table_score": 1.0,
64
+ "num_digit_samples": 3,
65
+ "num_cjk_samples": 0,
66
+ "num_table_samples": 3
67
+ }
68
+ },
69
+ "per_file": [
70
+ {
71
+ "file": "0001.png",
72
+ "category": "english_document",
73
+ "cer": 0.08904719501335707,
74
+ "wer": 0.24663677130044842,
75
+ "digit_cer": 0.023255813953488372,
76
+ "cjk_cer": 0.0,
77
+ "repetition_rate": 0.0,
78
+ "table_score": null,
79
+ "elapsed_seconds": 23.43824337499973,
80
+ "ref_length": 1345,
81
+ "hyp_length": 1259,
82
+ "ref_digit_count": 86,
83
+ "ref_cjk_count": 0
84
+ },
85
+ {
86
+ "file": "0002.png",
87
+ "category": "english_document",
88
+ "cer": 0.7802690582959642,
89
+ "wer": 0.41916167664670656,
90
+ "digit_cer": 0.042105263157894736,
91
+ "cjk_cer": 0.0,
92
+ "repetition_rate": 0.0,
93
+ "table_score": null,
94
+ "elapsed_seconds": 34.438602624999476,
95
+ "ref_length": 835,
96
+ "hyp_length": 1285,
97
+ "ref_digit_count": 95,
98
+ "ref_cjk_count": 0
99
+ },
100
+ {
101
+ "file": "0003.png",
102
+ "category": "table_numeric",
103
+ "cer": 0.17539863325740318,
104
+ "wer": 1.5,
105
+ "digit_cer": 0.1875,
106
+ "cjk_cer": 0.0,
107
+ "repetition_rate": 0.0,
108
+ "table_score": 1.0,
109
+ "elapsed_seconds": 13.494044040999142,
110
+ "ref_length": 446,
111
+ "hyp_length": 519,
112
+ "ref_digit_count": 80,
113
+ "ref_cjk_count": 0
114
+ },
115
+ {
116
+ "file": "0004.png",
117
+ "category": "table_numeric",
118
+ "cer": 0.17539863325740318,
119
+ "wer": 1.5,
120
+ "digit_cer": 0.1875,
121
+ "cjk_cer": 0.0,
122
+ "repetition_rate": 0.0,
123
+ "table_score": 1.0,
124
+ "elapsed_seconds": 12.677681540997582,
125
+ "ref_length": 446,
126
+ "hyp_length": 519,
127
+ "ref_digit_count": 80,
128
+ "ref_cjk_count": 0
129
+ },
130
+ {
131
+ "file": "0005.png",
132
+ "category": "table_numeric",
133
+ "cer": 0.17539863325740318,
134
+ "wer": 1.5,
135
+ "digit_cer": 0.1875,
136
+ "cjk_cer": 0.0,
137
+ "repetition_rate": 0.0,
138
+ "table_score": 1.0,
139
+ "elapsed_seconds": 12.359245291998377,
140
+ "ref_length": 446,
141
+ "hyp_length": 519,
142
+ "ref_digit_count": 80,
143
+ "ref_cjk_count": 0
144
+ },
145
+ {
146
+ "file": "0006.png",
147
+ "category": "cjk_document",
148
+ "cer": 0.0,
149
+ "wer": 0.0,
150
+ "digit_cer": 0.0,
151
+ "cjk_cer": 0.0,
152
+ "repetition_rate": 0.0,
153
+ "table_score": null,
154
+ "elapsed_seconds": 7.067715750003117,
155
+ "ref_length": 98,
156
+ "hyp_length": 98,
157
+ "ref_digit_count": 30,
158
+ "ref_cjk_count": 41
159
+ },
160
+ {
161
+ "file": "0007.png",
162
+ "category": "cjk_document",
163
+ "cer": 0.0,
164
+ "wer": 0.0,
165
+ "digit_cer": 0.0,
166
+ "cjk_cer": 0.0,
167
+ "repetition_rate": 0.0,
168
+ "table_score": null,
169
+ "elapsed_seconds": 7.551363666992984,
170
+ "ref_length": 100,
171
+ "hyp_length": 100,
172
+ "ref_digit_count": 30,
173
+ "ref_cjk_count": 42
174
+ },
175
+ {
176
+ "file": "0008.png",
177
+ "category": "cjk_document",
178
+ "cer": 0.5054945054945055,
179
+ "wer": 1.2272727272727273,
180
+ "digit_cer": 0.0,
181
+ "cjk_cer": 1.0,
182
+ "repetition_rate": 0.0,
183
+ "table_score": null,
184
+ "elapsed_seconds": 6.661412207991816,
185
+ "ref_length": 112,
186
+ "hyp_length": 118,
187
+ "ref_digit_count": 30,
188
+ "ref_cjk_count": 44
189
+ },
190
+ {
191
+ "file": "0009.png",
192
+ "category": "low_quality_scan",
193
+ "cer": 0.0,
194
+ "wer": 0.0,
195
+ "digit_cer": 0.0,
196
+ "cjk_cer": 0.0,
197
+ "repetition_rate": 0.0,
198
+ "table_score": null,
199
+ "elapsed_seconds": 6.571904916010681,
200
+ "ref_length": 120,
201
+ "hyp_length": 120,
202
+ "ref_digit_count": 29,
203
+ "ref_cjk_count": 0
204
+ },
205
+ {
206
+ "file": "0010.png",
207
+ "category": "low_quality_scan",
208
+ "cer": 0.0,
209
+ "wer": 0.0,
210
+ "digit_cer": 0.0,
211
+ "cjk_cer": 0.0,
212
+ "repetition_rate": 0.0,
213
+ "table_score": null,
214
+ "elapsed_seconds": 6.526005207997514,
215
+ "ref_length": 120,
216
+ "hyp_length": 120,
217
+ "ref_digit_count": 29,
218
+ "ref_cjk_count": 0
219
+ }
220
+ ]
221
+ }
release/calibration_bfloat16_performance.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16",
3
+ "served_revision": null,
4
+ "image_path": "test_invoice.png",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 256,
7
+ "system": {
8
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
9
+ "processor": "arm",
10
+ "python_version": "3.14.6",
11
+ "machine": "arm64",
12
+ "mlx_version": "0.32.0",
13
+ "mlx_vlm_version": "0.6.6",
14
+ "chip": "Apple M3 Max",
15
+ "total_memory_gb": 128.0
16
+ },
17
+ "model_load_time_seconds": 0.97142708300089,
18
+ "memory_after_load_mb": 4240.671875,
19
+ "num_warmup": 1,
20
+ "num_runs": 3,
21
+ "mean_tps": 38.770309481806514,
22
+ "std_tps": 0.6887167709523151,
23
+ "mean_elapsed_seconds": 8.391549889337815,
24
+ "mean_peak_memory_mb": 5420.697071,
25
+ "runs": [
26
+ {
27
+ "run": 1,
28
+ "elapsed_seconds": 8.60731708399544,
29
+ "tokens_generated": 256,
30
+ "tokens_generated_source": "mlx-vlm token count",
31
+ "tokens_per_second": 38.03899778227293,
32
+ "tokens_per_second_source": "mlx-vlm generation_tps",
33
+ "peak_memory_mb": 5420.521431,
34
+ "memory_delta_mb": 1337.1867550000002,
35
+ "process_peak_rss_mb": 4253.21875,
36
+ "prompt_tokens": 697,
37
+ "prompt_tokens_per_second": 380.356298056712,
38
+ "finish_reason": "length"
39
+ },
40
+ {
41
+ "run": 2,
42
+ "elapsed_seconds": 8.399659084010636,
43
+ "tokens_generated": 256,
44
+ "tokens_generated_source": "mlx-vlm token count",
45
+ "tokens_per_second": 38.57884453275097,
46
+ "tokens_per_second_source": "mlx-vlm generation_tps",
47
+ "peak_memory_mb": 5420.784891,
48
+ "memory_delta_mb": 1337.188071,
49
+ "process_peak_rss_mb": 4260.28125,
50
+ "prompt_tokens": 697,
51
+ "prompt_tokens_per_second": 401.47375057984203,
52
+ "finish_reason": "length"
53
+ },
54
+ {
55
+ "run": 3,
56
+ "elapsed_seconds": 8.167673500007368,
57
+ "tokens_generated": 256,
58
+ "tokens_generated_source": "mlx-vlm token count",
59
+ "tokens_per_second": 39.693086130395656,
60
+ "tokens_per_second_source": "mlx-vlm generation_tps",
61
+ "peak_memory_mb": 5420.784891,
62
+ "memory_delta_mb": 1337.188071,
63
+ "process_peak_rss_mb": 4260.46875,
64
+ "prompt_tokens": 697,
65
+ "prompt_tokens_per_second": 411.1295077060184,
66
+ "finish_reason": "length"
67
+ }
68
+ ]
69
+ }
release/calibration_mxfp8_accuracy.json ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8",
3
+ "served_revision": null,
4
+ "backend": "mlx",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 1024,
7
+ "profile": "accurate",
8
+ "generation_settings": {
9
+ "temperature": 0.0,
10
+ "top_p": 1.0,
11
+ "repetition_penalty": 1.0,
12
+ "no_repeat_ngram_size": 35,
13
+ "ngram_window": 128
14
+ },
15
+ "num_images": 10,
16
+ "num_samples": 10,
17
+ "total_time_seconds": 120.2266979579872,
18
+ "mean_cer": 0.21607021200652196,
19
+ "mean_wer": 0.7193288921545413,
20
+ "num_digit_samples": 10,
21
+ "num_cjk_samples": 3,
22
+ "mean_digit_cer": 0.1052422480620155,
23
+ "mean_cjk_cer": 0.3333333333333333,
24
+ "num_table_samples": 3,
25
+ "mean_table_score": 1.0,
26
+ "mean_repetition_rate": 0.0,
27
+ "by_category": {
28
+ "cjk_document": {
29
+ "num_samples": 3,
30
+ "mean_cer": 0.2490842490842491,
31
+ "mean_digit_cer": 0.08888888888888889,
32
+ "mean_cjk_cer": 0.3333333333333333,
33
+ "mean_table_score": null,
34
+ "num_digit_samples": 3,
35
+ "num_cjk_samples": 3,
36
+ "num_table_samples": 0
37
+ },
38
+ "english_document": {
39
+ "num_samples": 2,
40
+ "mean_cer": 0.44362673652013146,
41
+ "mean_digit_cer": 0.11162790697674418,
42
+ "mean_cjk_cer": null,
43
+ "mean_table_score": null,
44
+ "num_digit_samples": 2,
45
+ "num_cjk_samples": 0,
46
+ "num_table_samples": 0
47
+ },
48
+ "low_quality_scan": {
49
+ "num_samples": 2,
50
+ "mean_cer": 0.0,
51
+ "mean_digit_cer": 0.0,
52
+ "mean_cjk_cer": null,
53
+ "mean_table_score": null,
54
+ "num_digit_samples": 2,
55
+ "num_cjk_samples": 0,
56
+ "num_table_samples": 0
57
+ },
58
+ "table_numeric": {
59
+ "num_samples": 3,
60
+ "mean_cer": 0.1753986332574032,
61
+ "mean_digit_cer": 0.1875,
62
+ "mean_cjk_cer": null,
63
+ "mean_table_score": 1.0,
64
+ "num_digit_samples": 3,
65
+ "num_cjk_samples": 0,
66
+ "num_table_samples": 3
67
+ }
68
+ },
69
+ "per_file": [
70
+ {
71
+ "file": "0001.png",
72
+ "category": "english_document",
73
+ "cer": 0.08904719501335707,
74
+ "wer": 0.24663677130044842,
75
+ "digit_cer": 0.023255813953488372,
76
+ "cjk_cer": 0.0,
77
+ "repetition_rate": 0.0,
78
+ "table_score": null,
79
+ "elapsed_seconds": 21.754243709001457,
80
+ "ref_length": 1345,
81
+ "hyp_length": 1259,
82
+ "ref_digit_count": 86,
83
+ "ref_cjk_count": 0
84
+ },
85
+ {
86
+ "file": "0002.png",
87
+ "category": "english_document",
88
+ "cer": 0.7982062780269058,
89
+ "wer": 0.40119760479041916,
90
+ "digit_cer": 0.2,
91
+ "cjk_cer": 1.0,
92
+ "repetition_rate": 0.0,
93
+ "table_score": null,
94
+ "elapsed_seconds": 30.940298208006425,
95
+ "ref_length": 835,
96
+ "hyp_length": 1293,
97
+ "ref_digit_count": 95,
98
+ "ref_cjk_count": 0
99
+ },
100
+ {
101
+ "file": "0003.png",
102
+ "category": "table_numeric",
103
+ "cer": 0.17539863325740318,
104
+ "wer": 1.5,
105
+ "digit_cer": 0.1875,
106
+ "cjk_cer": 0.0,
107
+ "repetition_rate": 0.0,
108
+ "table_score": 1.0,
109
+ "elapsed_seconds": 11.853719249993446,
110
+ "ref_length": 446,
111
+ "hyp_length": 519,
112
+ "ref_digit_count": 80,
113
+ "ref_cjk_count": 0
114
+ },
115
+ {
116
+ "file": "0004.png",
117
+ "category": "table_numeric",
118
+ "cer": 0.17539863325740318,
119
+ "wer": 1.5,
120
+ "digit_cer": 0.1875,
121
+ "cjk_cer": 0.0,
122
+ "repetition_rate": 0.0,
123
+ "table_score": 1.0,
124
+ "elapsed_seconds": 11.799329292000039,
125
+ "ref_length": 446,
126
+ "hyp_length": 519,
127
+ "ref_digit_count": 80,
128
+ "ref_cjk_count": 0
129
+ },
130
+ {
131
+ "file": "0005.png",
132
+ "category": "table_numeric",
133
+ "cer": 0.17539863325740318,
134
+ "wer": 1.5,
135
+ "digit_cer": 0.1875,
136
+ "cjk_cer": 0.0,
137
+ "repetition_rate": 0.0,
138
+ "table_score": 1.0,
139
+ "elapsed_seconds": 11.05323970799509,
140
+ "ref_length": 446,
141
+ "hyp_length": 519,
142
+ "ref_digit_count": 80,
143
+ "ref_cjk_count": 0
144
+ },
145
+ {
146
+ "file": "0006.png",
147
+ "category": "cjk_document",
148
+ "cer": 0.0,
149
+ "wer": 0.0,
150
+ "digit_cer": 0.0,
151
+ "cjk_cer": 0.0,
152
+ "repetition_rate": 0.0,
153
+ "table_score": null,
154
+ "elapsed_seconds": 6.7778485829912825,
155
+ "ref_length": 98,
156
+ "hyp_length": 98,
157
+ "ref_digit_count": 30,
158
+ "ref_cjk_count": 41
159
+ },
160
+ {
161
+ "file": "0007.png",
162
+ "category": "cjk_document",
163
+ "cer": 0.0,
164
+ "wer": 0.0,
165
+ "digit_cer": 0.0,
166
+ "cjk_cer": 0.0,
167
+ "repetition_rate": 0.0,
168
+ "table_score": null,
169
+ "elapsed_seconds": 6.7498448330006795,
170
+ "ref_length": 100,
171
+ "hyp_length": 100,
172
+ "ref_digit_count": 30,
173
+ "ref_cjk_count": 42
174
+ },
175
+ {
176
+ "file": "0008.png",
177
+ "category": "cjk_document",
178
+ "cer": 0.7472527472527473,
179
+ "wer": 2.0454545454545454,
180
+ "digit_cer": 0.26666666666666666,
181
+ "cjk_cer": 1.0,
182
+ "repetition_rate": 0.0,
183
+ "table_score": null,
184
+ "elapsed_seconds": 6.956620541997836,
185
+ "ref_length": 112,
186
+ "hyp_length": 146,
187
+ "ref_digit_count": 30,
188
+ "ref_cjk_count": 44
189
+ },
190
+ {
191
+ "file": "0009.png",
192
+ "category": "low_quality_scan",
193
+ "cer": 0.0,
194
+ "wer": 0.0,
195
+ "digit_cer": 0.0,
196
+ "cjk_cer": 0.0,
197
+ "repetition_rate": 0.0,
198
+ "table_score": null,
199
+ "elapsed_seconds": 6.085180042005959,
200
+ "ref_length": 120,
201
+ "hyp_length": 120,
202
+ "ref_digit_count": 29,
203
+ "ref_cjk_count": 0
204
+ },
205
+ {
206
+ "file": "0010.png",
207
+ "category": "low_quality_scan",
208
+ "cer": 0.0,
209
+ "wer": 0.0,
210
+ "digit_cer": 0.0,
211
+ "cjk_cer": 0.0,
212
+ "repetition_rate": 0.0,
213
+ "table_score": null,
214
+ "elapsed_seconds": 6.256373790994985,
215
+ "ref_length": 120,
216
+ "hyp_length": 120,
217
+ "ref_digit_count": 29,
218
+ "ref_cjk_count": 0
219
+ }
220
+ ]
221
+ }
release/calibration_mxfp8_performance.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8",
3
+ "served_revision": null,
4
+ "image_path": "test_invoice.png",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 256,
7
+ "system": {
8
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
9
+ "processor": "arm",
10
+ "python_version": "3.14.6",
11
+ "machine": "arm64",
12
+ "mlx_version": "0.32.0",
13
+ "mlx_vlm_version": "0.6.6",
14
+ "chip": "Apple M3 Max",
15
+ "total_memory_gb": 128.0
16
+ },
17
+ "model_load_time_seconds": 1.0123953330039512,
18
+ "memory_after_load_mb": 4115.359375,
19
+ "num_warmup": 1,
20
+ "num_runs": 3,
21
+ "mean_tps": 41.326731942543184,
22
+ "std_tps": 1.2149507723608595,
23
+ "mean_elapsed_seconds": 7.814036472002044,
24
+ "mean_peak_memory_mb": 5260.396014999999,
25
+ "runs": [
26
+ {
27
+ "run": 1,
28
+ "elapsed_seconds": 7.896335166005883,
29
+ "tokens_generated": 256,
30
+ "tokens_generated_source": "mlx-vlm token count",
31
+ "tokens_per_second": 41.34150964421114,
32
+ "tokens_per_second_source": "mlx-vlm generation_tps",
33
+ "peak_memory_mb": 5260.220375,
34
+ "memory_delta_mb": 1337.1867549999997,
35
+ "process_peak_rss_mb": 4129.09375,
36
+ "prompt_tokens": 697,
37
+ "prompt_tokens_per_second": 421.22509117788985,
38
+ "finish_reason": "length"
39
+ },
40
+ {
41
+ "run": 2,
42
+ "elapsed_seconds": 7.41755587499938,
43
+ "tokens_generated": 256,
44
+ "tokens_generated_source": "mlx-vlm token count",
45
+ "tokens_per_second": 42.80729278290862,
46
+ "tokens_per_second_source": "mlx-vlm generation_tps",
47
+ "peak_memory_mb": 5260.483835,
48
+ "memory_delta_mb": 1337.188071,
49
+ "process_peak_rss_mb": 4137.328125,
50
+ "prompt_tokens": 697,
51
+ "prompt_tokens_per_second": 496.01062497025777,
52
+ "finish_reason": "length"
53
+ },
54
+ {
55
+ "run": 3,
56
+ "elapsed_seconds": 8.12821837500087,
57
+ "tokens_generated": 256,
58
+ "tokens_generated_source": "mlx-vlm token count",
59
+ "tokens_per_second": 39.83139340050979,
60
+ "tokens_per_second_source": "mlx-vlm generation_tps",
61
+ "peak_memory_mb": 5260.483835,
62
+ "memory_delta_mb": 1337.188071,
63
+ "process_peak_rss_mb": 4137.578125,
64
+ "prompt_tokens": 697,
65
+ "prompt_tokens_per_second": 415.31823155489394,
66
+ "finish_reason": "length"
67
+ }
68
+ ]
69
+ }
release/calibration_reference_performance.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
3
+ "served_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
4
+ "image_path": "test_invoice.png",
5
+ "prompt": "<image>document parsing.",
6
+ "max_tokens": 256,
7
+ "system": {
8
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
9
+ "processor": "arm",
10
+ "python_version": "3.14.6",
11
+ "machine": "arm64",
12
+ "mlx_version": "0.32.0",
13
+ "mlx_vlm_version": "0.6.6",
14
+ "chip": "Apple M3 Max",
15
+ "total_memory_gb": 128.0
16
+ },
17
+ "model_load_time_seconds": 5.673565958000836,
18
+ "memory_after_load_mb": 3994.578125,
19
+ "num_warmup": 1,
20
+ "num_runs": 3,
21
+ "mean_tps": 40.402663236114016,
22
+ "std_tps": 0.3014552552023151,
23
+ "mean_elapsed_seconds": 8.03746888867075,
24
+ "mean_peak_memory_mb": 5165.991419999999,
25
+ "runs": [
26
+ {
27
+ "run": 1,
28
+ "elapsed_seconds": 8.067712333009695,
29
+ "tokens_generated": 256,
30
+ "tokens_generated_source": "mlx-vlm token count",
31
+ "tokens_per_second": 40.079502760011906,
32
+ "tokens_per_second_source": "mlx-vlm generation_tps",
33
+ "peak_memory_mb": 5165.81578,
34
+ "memory_delta_mb": 1337.186768,
35
+ "process_peak_rss_mb": 4016.75,
36
+ "prompt_tokens": 697,
37
+ "prompt_tokens_per_second": 422.0660830294658,
38
+ "finish_reason": "length"
39
+ },
40
+ {
41
+ "run": 2,
42
+ "elapsed_seconds": 8.119986582998536,
43
+ "tokens_generated": 256,
44
+ "tokens_generated_source": "mlx-vlm token count",
45
+ "tokens_per_second": 40.323435695329465,
46
+ "tokens_per_second_source": "mlx-vlm generation_tps",
47
+ "peak_memory_mb": 5166.07924,
48
+ "memory_delta_mb": 1337.188084,
49
+ "process_peak_rss_mb": 4025.171875,
50
+ "prompt_tokens": 697,
51
+ "prompt_tokens_per_second": 399.62688128036274,
52
+ "finish_reason": "length"
53
+ },
54
+ {
55
+ "run": 3,
56
+ "elapsed_seconds": 7.924707750004018,
57
+ "tokens_generated": 256,
58
+ "tokens_generated_source": "mlx-vlm token count",
59
+ "tokens_per_second": 40.80505125300066,
60
+ "tokens_per_second_source": "mlx-vlm generation_tps",
61
+ "peak_memory_mb": 5166.07924,
62
+ "memory_delta_mb": 1337.188084,
63
+ "process_peak_rss_mb": 4025.484375,
64
+ "prompt_tokens": 697,
65
+ "prompt_tokens_per_second": 429.46142333454605,
66
+ "finish_reason": "length"
67
+ }
68
+ ]
69
+ }
release/calibration_results.json CHANGED
@@ -1,38 +1,40 @@
1
  {
2
- "schema_version": 1,
3
- "created_at": "2026-07-20T23:25:55.306169+00:00",
4
  "target_pattern": "language_model.lm_head",
5
  "selection_policy": "fastest candidate passing existing quality and throughput limits",
6
  "thresholds": {
7
- "max_cer_delta_vs_bf16": 0.01,
8
  "max_cer_delta_vs_reference": 0.005,
9
  "max_digit_cer_delta_vs_bf16": 0.01,
10
  "max_table_score_degradation_vs_bf16": 0.01,
11
  "min_tps_ratio_vs_reference": 0.9,
12
- "max_weight_size_gb": 4.5
 
 
13
  },
14
  "experiments": [
15
  {
16
  "label": "bf16-head",
17
  "precision": "bfloat16",
18
- "passed": false,
19
  "checks": {
20
  "cer": true,
21
  "digit_cer": true,
22
  "table_score": true,
23
- "throughput": false
24
  },
25
  "metrics": {
26
- "mean_cer": 0.23190356458231498,
27
- "mean_digit_cer": 0.08222376405546196,
28
  "mean_table_score": 1.0,
29
- "mean_tps": 271.0950821181683
30
  },
31
  "deltas": {
32
- "cer_vs_bf16": -0.007284148710181432,
33
- "digit_cer_vs_bf16": -0.005116203514816328,
34
  "table_degradation_vs_bf16": 0.0,
35
- "tps_ratio_vs_reference": 0.8668704848166195
36
  }
37
  },
38
  {
@@ -40,22 +42,22 @@
40
  "precision": "mxfp8",
41
  "passed": false,
42
  "checks": {
43
- "cer": false,
44
- "digit_cer": true,
45
  "table_score": true,
46
  "throughput": true
47
  },
48
  "metrics": {
49
- "mean_cer": 0.26017003915600867,
50
- "mean_digit_cer": 0.0769606061607251,
51
  "mean_table_score": 1.0,
52
- "mean_tps": 307.3938836867251
53
  },
54
  "deltas": {
55
- "cer_vs_bf16": 0.020982325863512258,
56
- "digit_cer_vs_bf16": -0.010379361409553187,
57
  "table_degradation_vs_bf16": 0.0,
58
- "tps_ratio_vs_reference": 0.9829417889071937
59
  }
60
  },
61
  {
@@ -69,16 +71,16 @@
69
  "throughput": true
70
  },
71
  "metrics": {
72
- "mean_cer": 0.23738438172232493,
73
- "mean_digit_cer": 0.08573253598528652,
74
  "mean_table_score": 1.0,
75
- "mean_tps": 301.24796539578256
76
  },
77
  "deltas": {
78
- "cer_vs_bf16": -0.0018033315701714847,
79
- "digit_cer_vs_bf16": -0.0016074315849917697,
80
  "table_degradation_vs_bf16": 0.0,
81
- "tps_ratio_vs_reference": 0.9632892185732532
82
  }
83
  }
84
  ],
@@ -88,5 +90,164 @@
88
  },
89
  "precision_overrides": {
90
  "language_model.lm_head": "affine8"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
  }
 
1
  {
2
+ "schema_version": 3,
3
+ "created_at": "2026-07-21T05:26:12.430585+00:00",
4
  "target_pattern": "language_model.lm_head",
5
  "selection_policy": "fastest candidate passing existing quality and throughput limits",
6
  "thresholds": {
7
+ "max_cer_delta_vs_bf16": 0.015,
8
  "max_cer_delta_vs_reference": 0.005,
9
  "max_digit_cer_delta_vs_bf16": 0.01,
10
  "max_table_score_degradation_vs_bf16": 0.01,
11
  "min_tps_ratio_vs_reference": 0.9,
12
+ "max_weight_size_gb": 4.5,
13
+ "min_rswa_tokens": 8192,
14
+ "max_rswa_repetition_rate": 0.25
15
  },
16
  "experiments": [
17
  {
18
  "label": "bf16-head",
19
  "precision": "bfloat16",
20
+ "passed": true,
21
  "checks": {
22
  "cer": true,
23
  "digit_cer": true,
24
  "table_score": true,
25
+ "throughput": true
26
  },
27
  "metrics": {
28
+ "mean_cer": 0.19010066585760363,
29
+ "mean_digit_cer": 0.06278610771113831,
30
  "mean_table_score": 1.0,
31
+ "mean_tps": 38.770309481806514
32
  },
33
  "deltas": {
34
+ "cer_vs_bf16": -0.012208451030632145,
35
+ "digit_cer_vs_bf16": -0.00958384332925337,
36
  "table_degradation_vs_bf16": 0.0,
37
+ "tps_ratio_vs_reference": 0.9595978674779929
38
  }
39
  },
40
  {
 
42
  "precision": "mxfp8",
43
  "passed": false,
44
  "checks": {
45
+ "cer": true,
46
+ "digit_cer": false,
47
  "table_score": true,
48
  "throughput": true
49
  },
50
  "metrics": {
51
+ "mean_cer": 0.21607021200652196,
52
+ "mean_digit_cer": 0.1052422480620155,
53
  "mean_table_score": 1.0,
54
+ "mean_tps": 41.326731942543184
55
  },
56
  "deltas": {
57
+ "cer_vs_bf16": 0.013761095118286187,
58
+ "digit_cer_vs_bf16": 0.03287229702162382,
59
  "table_degradation_vs_bf16": 0.0,
60
+ "tps_ratio_vs_reference": 1.0228714800563738
61
  }
62
  },
63
  {
 
71
  "throughput": true
72
  },
73
  "metrics": {
74
+ "mean_cer": 0.19105830313811087,
75
+ "mean_digit_cer": 0.07857558139534884,
76
  "mean_table_score": 1.0,
77
+ "mean_tps": 39.58614991640946
78
  },
79
  "deltas": {
80
+ "cer_vs_bf16": -0.011250813750124905,
81
+ "digit_cer_vs_bf16": 0.0062056303549571645,
82
  "table_degradation_vs_bf16": 0.0,
83
+ "tps_ratio_vs_reference": 0.979790606502031
84
  }
85
  }
86
  ],
 
90
  },
91
  "precision_overrides": {
92
  "language_model.lm_head": "affine8"
93
+ },
94
+ "input_artifacts": {
95
+ "calibration_baseline_accuracy": {
96
+ "filename": "calibration_baseline_accuracy.json",
97
+ "size": 5680,
98
+ "sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
99
+ },
100
+ "calibration_reference_performance": {
101
+ "filename": "calibration_reference_performance.json",
102
+ "size": 2271,
103
+ "sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
104
+ },
105
+ "calibration_bfloat16_accuracy": {
106
+ "filename": "calibration_bfloat16_accuracy.json",
107
+ "size": 5678,
108
+ "sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
109
+ },
110
+ "calibration_bfloat16_performance": {
111
+ "filename": "calibration_bfloat16_performance.json",
112
+ "size": 2244,
113
+ "sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
114
+ },
115
+ "calibration_mxfp8_accuracy": {
116
+ "filename": "calibration_mxfp8_accuracy.json",
117
+ "size": 5689,
118
+ "sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
119
+ },
120
+ "calibration_mxfp8_performance": {
121
+ "filename": "calibration_mxfp8_performance.json",
122
+ "size": 2252,
123
+ "sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
124
+ },
125
+ "calibration_affine8_accuracy": {
126
+ "filename": "calibration_affine8_accuracy.json",
127
+ "size": 5657,
128
+ "sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
129
+ },
130
+ "calibration_affine8_performance": {
131
+ "filename": "calibration_affine8_performance.json",
132
+ "size": 2247,
133
+ "sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
134
+ }
135
+ },
136
+ "dataset": {
137
+ "name": "calibration",
138
+ "num_samples": 10,
139
+ "num_digit_samples": 10,
140
+ "num_cjk_samples": 3,
141
+ "num_table_samples": 3,
142
+ "category_counts": {
143
+ "cjk_document": 3,
144
+ "english_document": 2,
145
+ "low_quality_scan": 2,
146
+ "table_numeric": 3
147
+ },
148
+ "manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
149
+ "content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
150
+ "files": [
151
+ {
152
+ "path": "images/0001.png",
153
+ "size": 243645,
154
+ "sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
155
+ },
156
+ {
157
+ "path": "ground_truth/0001.txt",
158
+ "size": 1346,
159
+ "sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
160
+ },
161
+ {
162
+ "path": "images/0002.png",
163
+ "size": 208699,
164
+ "sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
165
+ },
166
+ {
167
+ "path": "ground_truth/0002.txt",
168
+ "size": 838,
169
+ "sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
170
+ },
171
+ {
172
+ "path": "images/0003.png",
173
+ "size": 40039,
174
+ "sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
175
+ },
176
+ {
177
+ "path": "ground_truth/0003.txt",
178
+ "size": 459,
179
+ "sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
180
+ },
181
+ {
182
+ "path": "images/0004.png",
183
+ "size": 40356,
184
+ "sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
185
+ },
186
+ {
187
+ "path": "ground_truth/0004.txt",
188
+ "size": 459,
189
+ "sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
190
+ },
191
+ {
192
+ "path": "images/0005.png",
193
+ "size": 40650,
194
+ "sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
195
+ },
196
+ {
197
+ "path": "ground_truth/0005.txt",
198
+ "size": 459,
199
+ "sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
200
+ },
201
+ {
202
+ "path": "images/0006.png",
203
+ "size": 46036,
204
+ "sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
205
+ },
206
+ {
207
+ "path": "ground_truth/0006.txt",
208
+ "size": 191,
209
+ "sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
210
+ },
211
+ {
212
+ "path": "images/0007.png",
213
+ "size": 41828,
214
+ "sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
215
+ },
216
+ {
217
+ "path": "ground_truth/0007.txt",
218
+ "size": 195,
219
+ "sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
220
+ },
221
+ {
222
+ "path": "images/0008.png",
223
+ "size": 20975,
224
+ "sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
225
+ },
226
+ {
227
+ "path": "ground_truth/0008.txt",
228
+ "size": 201,
229
+ "sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
230
+ },
231
+ {
232
+ "path": "images/0009.png",
233
+ "size": 487313,
234
+ "sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
235
+ },
236
+ {
237
+ "path": "ground_truth/0009.txt",
238
+ "size": 121,
239
+ "sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
240
+ },
241
+ {
242
+ "path": "images/0010.png",
243
+ "size": 515820,
244
+ "sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
245
+ },
246
+ {
247
+ "path": "ground_truth/0010.txt",
248
+ "size": 121,
249
+ "sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
250
+ }
251
+ ]
252
  }
253
  }
release/candidate_accuracy.json CHANGED
@@ -1,5 +1,7 @@
1
  {
2
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
 
 
3
  "prompt": "<image>document parsing.",
4
  "max_tokens": 1024,
5
  "profile": "accurate",
@@ -12,183 +14,237 @@
12
  },
13
  "num_images": 12,
14
  "num_samples": 12,
15
- "total_time_seconds": 31.473419997957535,
16
- "mean_cer": 0.23738438172232493,
17
- "mean_wer": 0.5707107552516416,
18
  "num_digit_samples": 12,
19
  "num_cjk_samples": 3,
20
- "mean_digit_cer": 0.08573253598528652,
21
- "mean_cjk_cer": 0.3333333333333333,
22
  "num_table_samples": 3,
23
  "mean_table_score": 1.0,
24
  "mean_repetition_rate": 0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  "per_file": [
26
  {
27
  "file": "0001.png",
28
- "cer": 0.020480854853072127,
29
- "wer": 0.18385650224215247,
30
- "digit_cer": 0.0,
 
31
  "cjk_cer": 0.0,
32
  "repetition_rate": 0.0,
33
  "table_score": null,
34
- "elapsed_seconds": 3.815771791996667,
35
- "ref_length": 1345,
36
- "hyp_length": 1349,
37
- "ref_digit_count": 86,
38
  "ref_cjk_count": 0
39
  },
40
  {
41
  "file": "0002.png",
42
- "cer": 0.9327354260089686,
43
- "wer": 0.5089820359281437,
44
- "digit_cer": 0.17894736842105263,
 
45
  "cjk_cer": 0.0,
46
  "repetition_rate": 0.0,
47
  "table_score": null,
48
- "elapsed_seconds": 4.953415083000436,
49
- "ref_length": 835,
50
- "hyp_length": 1310,
51
- "ref_digit_count": 95,
52
  "ref_cjk_count": 0
53
  },
54
  {
55
  "file": "0003.png",
56
- "cer": 0.3512064343163539,
57
- "wer": 0.3317757009345794,
58
- "digit_cer": 0.14814814814814814,
 
59
  "cjk_cer": 0.0,
60
  "repetition_rate": 0.0,
61
  "table_score": null,
62
- "elapsed_seconds": 3.5391229999950156,
63
- "ref_length": 1332,
64
- "hyp_length": 1672,
65
- "ref_digit_count": 27,
66
  "ref_cjk_count": 0
67
  },
68
  {
69
  "file": "0004.png",
70
- "cer": 0.38176895306859204,
71
- "wer": 0.36936936936936937,
72
- "digit_cer": 0.1016949152542373,
 
73
  "cjk_cer": 0.0,
74
  "repetition_rate": 0.0,
75
  "table_score": null,
76
- "elapsed_seconds": 3.735332540993113,
77
- "ref_length": 1329,
78
- "hyp_length": 1705,
79
- "ref_digit_count": 59,
80
  "ref_cjk_count": 0
81
  },
82
  {
83
  "file": "0005.png",
84
- "cer": 0.17715617715617715,
 
85
  "wer": 1.5,
86
- "digit_cer": 0.2,
87
  "cjk_cer": 0.0,
88
  "repetition_rate": 0.0,
89
  "table_score": 1.0,
90
- "elapsed_seconds": 2.769828624994261,
91
- "ref_length": 436,
92
- "hyp_length": 508,
93
- "ref_digit_count": 70,
94
  "ref_cjk_count": 0
95
  },
96
  {
97
  "file": "0006.png",
98
- "cer": 0.17715617715617715,
 
99
  "wer": 1.5,
100
- "digit_cer": 0.2,
101
  "cjk_cer": 0.0,
102
  "repetition_rate": 0.0,
103
  "table_score": 1.0,
104
- "elapsed_seconds": 2.7706372079992434,
105
- "ref_length": 436,
106
- "hyp_length": 508,
107
- "ref_digit_count": 70,
108
  "ref_cjk_count": 0
109
  },
110
  {
111
  "file": "0007.png",
112
- "cer": 0.17715617715617715,
 
113
  "wer": 1.5,
114
- "digit_cer": 0.2,
115
  "cjk_cer": 0.0,
116
  "repetition_rate": 0.0,
117
  "table_score": 1.0,
118
- "elapsed_seconds": 2.726061583001865,
119
- "ref_length": 436,
120
- "hyp_length": 508,
121
- "ref_digit_count": 70,
122
  "ref_cjk_count": 0
123
  },
124
  {
125
  "file": "0008.png",
 
126
  "cer": 0.0,
127
  "wer": 0.0,
128
  "digit_cer": 0.0,
129
  "cjk_cer": 0.0,
130
  "repetition_rate": 0.0,
131
  "table_score": null,
132
- "elapsed_seconds": 1.4497268339910079,
133
- "ref_length": 92,
134
- "hyp_length": 92,
135
- "ref_digit_count": 26,
136
  "ref_cjk_count": 41
137
  },
138
  {
139
  "file": "0009.png",
140
- "cer": 0.0,
141
- "wer": 0.0,
 
142
  "digit_cer": 0.0,
143
- "cjk_cer": 0.0,
144
  "repetition_rate": 0.0,
145
  "table_score": null,
146
- "elapsed_seconds": 1.4638195829902543,
147
- "ref_length": 93,
148
- "hyp_length": 93,
149
- "ref_digit_count": 25,
150
  "ref_cjk_count": 42
151
  },
152
  {
153
  "file": "0010.png",
154
- "cer": 0.6309523809523809,
155
- "wer": 0.9545454545454546,
156
- "digit_cer": 0.0,
 
157
  "cjk_cer": 1.0,
158
  "repetition_rate": 0.0,
159
  "table_score": null,
160
- "elapsed_seconds": 1.4698359579924727,
161
- "ref_length": 105,
162
- "hyp_length": 110,
163
- "ref_digit_count": 25,
164
  "ref_cjk_count": 44
165
  },
166
  {
167
  "file": "0011.png",
 
168
  "cer": 0.0,
169
  "wer": 0.0,
170
  "digit_cer": 0.0,
171
  "cjk_cer": 0.0,
172
  "repetition_rate": 0.0,
173
  "table_score": null,
174
- "elapsed_seconds": 1.3830854999978328,
175
- "ref_length": 118,
176
- "hyp_length": 118,
177
- "ref_digit_count": 27,
178
  "ref_cjk_count": 0
179
  },
180
  {
181
  "file": "0012.png",
 
182
  "cer": 0.0,
183
  "wer": 0.0,
184
  "digit_cer": 0.0,
185
  "cjk_cer": 0.0,
186
  "repetition_rate": 0.0,
187
  "table_score": null,
188
- "elapsed_seconds": 1.3967822910053656,
189
- "ref_length": 118,
190
- "hyp_length": 118,
191
- "ref_digit_count": 27,
192
  "ref_cjk_count": 0
193
  }
194
  ]
 
1
  {
2
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
3
+ "served_revision": null,
4
+ "backend": "mlx",
5
  "prompt": "<image>document parsing.",
6
  "max_tokens": 1024,
7
  "profile": "accurate",
 
14
  },
15
  "num_images": 12,
16
  "num_samples": 12,
17
+ "total_time_seconds": 164.14354283601278,
18
+ "mean_cer": 0.21633091868789586,
19
+ "mean_wer": 0.6718835226038614,
20
  "num_digit_samples": 12,
21
  "num_cjk_samples": 3,
22
+ "mean_digit_cer": 0.11193906322597019,
23
+ "mean_cjk_cer": 0.3412698412698412,
24
  "num_table_samples": 3,
25
  "mean_table_score": 1.0,
26
  "mean_repetition_rate": 0.0,
27
+ "by_category": {
28
+ "cjk_document": {
29
+ "num_samples": 3,
30
+ "mean_cer": 0.2455035601103017,
31
+ "mean_digit_cer": 0.08888888888888889,
32
+ "mean_cjk_cer": 0.3412698412698412,
33
+ "mean_table_score": null,
34
+ "num_digit_samples": 3,
35
+ "num_cjk_samples": 3,
36
+ "num_table_samples": 0
37
+ },
38
+ "english_document": {
39
+ "num_samples": 4,
40
+ "mean_cer": 0.3333161110379089,
41
+ "mean_digit_cer": 0.12852552301124393,
42
+ "mean_cjk_cer": null,
43
+ "mean_table_score": null,
44
+ "num_digit_samples": 4,
45
+ "num_cjk_samples": 0,
46
+ "num_table_samples": 0
47
+ },
48
+ "low_quality_scan": {
49
+ "num_samples": 2,
50
+ "mean_cer": 0.0,
51
+ "mean_digit_cer": 0.0,
52
+ "mean_cjk_cer": null,
53
+ "mean_table_score": null,
54
+ "num_digit_samples": 2,
55
+ "num_cjk_samples": 0,
56
+ "num_table_samples": 0
57
+ },
58
+ "table_numeric": {
59
+ "num_samples": 3,
60
+ "mean_cer": 0.1753986332574032,
61
+ "mean_digit_cer": 0.1875,
62
+ "mean_cjk_cer": null,
63
+ "mean_table_score": 1.0,
64
+ "num_digit_samples": 3,
65
+ "num_cjk_samples": 0,
66
+ "num_table_samples": 3
67
+ }
68
+ },
69
  "per_file": [
70
  {
71
  "file": "0001.png",
72
+ "category": "english_document",
73
+ "cer": 0.3494191242180518,
74
+ "wer": 0.3317757009345794,
75
+ "digit_cer": 0.14814814814814814,
76
  "cjk_cer": 0.0,
77
  "repetition_rate": 0.0,
78
  "table_score": null,
79
+ "elapsed_seconds": 23.19513687500148,
80
+ "ref_length": 1332,
81
+ "hyp_length": 1669,
82
+ "ref_digit_count": 27,
83
  "ref_cjk_count": 0
84
  },
85
  {
86
  "file": "0002.png",
87
+ "category": "english_document",
88
+ "cer": 0.37815884476534295,
89
+ "wer": 0.36486486486486486,
90
+ "digit_cer": 0.1016949152542373,
91
  "cjk_cer": 0.0,
92
  "repetition_rate": 0.0,
93
  "table_score": null,
94
+ "elapsed_seconds": 23.631959624995943,
95
+ "ref_length": 1329,
96
+ "hyp_length": 1708,
97
+ "ref_digit_count": 59,
98
  "ref_cjk_count": 0
99
  },
100
  {
101
  "file": "0003.png",
102
+ "category": "english_document",
103
+ "cer": 0.5393474088291746,
104
+ "wer": 0.5086206896551724,
105
+ "digit_cer": 0.136986301369863,
106
  "cjk_cer": 0.0,
107
  "repetition_rate": 0.0,
108
  "table_score": null,
109
+ "elapsed_seconds": 25.065509457999724,
110
+ "ref_length": 636,
111
+ "hyp_length": 804,
112
+ "ref_digit_count": 73,
113
  "ref_cjk_count": 0
114
  },
115
  {
116
  "file": "0004.png",
117
+ "category": "english_document",
118
+ "cer": 0.06633906633906633,
119
+ "wer": 0.18309859154929578,
120
+ "digit_cer": 0.12727272727272726,
121
  "cjk_cer": 0.0,
122
  "repetition_rate": 0.0,
123
  "table_score": null,
124
+ "elapsed_seconds": 18.39167737500975,
125
+ "ref_length": 477,
126
+ "hyp_length": 484,
127
+ "ref_digit_count": 55,
128
  "ref_cjk_count": 0
129
  },
130
  {
131
  "file": "0005.png",
132
+ "category": "table_numeric",
133
+ "cer": 0.17539863325740318,
134
  "wer": 1.5,
135
+ "digit_cer": 0.1875,
136
  "cjk_cer": 0.0,
137
  "repetition_rate": 0.0,
138
  "table_score": 1.0,
139
+ "elapsed_seconds": 12.358092667011078,
140
+ "ref_length": 446,
141
+ "hyp_length": 519,
142
+ "ref_digit_count": 80,
143
  "ref_cjk_count": 0
144
  },
145
  {
146
  "file": "0006.png",
147
+ "category": "table_numeric",
148
+ "cer": 0.17539863325740318,
149
  "wer": 1.5,
150
+ "digit_cer": 0.1875,
151
  "cjk_cer": 0.0,
152
  "repetition_rate": 0.0,
153
  "table_score": 1.0,
154
+ "elapsed_seconds": 12.517006292007864,
155
+ "ref_length": 446,
156
+ "hyp_length": 519,
157
+ "ref_digit_count": 80,
158
  "ref_cjk_count": 0
159
  },
160
  {
161
  "file": "0007.png",
162
+ "category": "table_numeric",
163
+ "cer": 0.17539863325740318,
164
  "wer": 1.5,
165
+ "digit_cer": 0.1875,
166
  "cjk_cer": 0.0,
167
  "repetition_rate": 0.0,
168
  "table_score": 1.0,
169
+ "elapsed_seconds": 12.23163116700016,
170
+ "ref_length": 446,
171
+ "hyp_length": 519,
172
+ "ref_digit_count": 80,
173
  "ref_cjk_count": 0
174
  },
175
  {
176
  "file": "0008.png",
177
+ "category": "cjk_document",
178
  "cer": 0.0,
179
  "wer": 0.0,
180
  "digit_cer": 0.0,
181
  "cjk_cer": 0.0,
182
  "repetition_rate": 0.0,
183
  "table_score": null,
184
+ "elapsed_seconds": 7.415536249987781,
185
+ "ref_length": 98,
186
+ "hyp_length": 98,
187
+ "ref_digit_count": 30,
188
  "ref_cjk_count": 41
189
  },
190
  {
191
  "file": "0009.png",
192
+ "category": "cjk_document",
193
+ "cer": 0.011235955056179775,
194
+ "wer": 0.08333333333333333,
195
  "digit_cer": 0.0,
196
+ "cjk_cer": 0.023809523809523808,
197
  "repetition_rate": 0.0,
198
  "table_score": null,
199
+ "elapsed_seconds": 8.263199041990447,
200
+ "ref_length": 100,
201
+ "hyp_length": 100,
202
+ "ref_digit_count": 30,
203
  "ref_cjk_count": 42
204
  },
205
  {
206
  "file": "0010.png",
207
+ "category": "cjk_document",
208
+ "cer": 0.7252747252747253,
209
+ "wer": 2.090909090909091,
210
+ "digit_cer": 0.26666666666666666,
211
  "cjk_cer": 1.0,
212
  "repetition_rate": 0.0,
213
  "table_score": null,
214
+ "elapsed_seconds": 7.9901230840041535,
215
+ "ref_length": 112,
216
+ "hyp_length": 147,
217
+ "ref_digit_count": 30,
218
  "ref_cjk_count": 44
219
  },
220
  {
221
  "file": "0011.png",
222
+ "category": "low_quality_scan",
223
  "cer": 0.0,
224
  "wer": 0.0,
225
  "digit_cer": 0.0,
226
  "cjk_cer": 0.0,
227
  "repetition_rate": 0.0,
228
  "table_score": null,
229
+ "elapsed_seconds": 6.431838709002477,
230
+ "ref_length": 120,
231
+ "hyp_length": 120,
232
+ "ref_digit_count": 29,
233
  "ref_cjk_count": 0
234
  },
235
  {
236
  "file": "0012.png",
237
+ "category": "low_quality_scan",
238
  "cer": 0.0,
239
  "wer": 0.0,
240
  "digit_cer": 0.0,
241
  "cjk_cer": 0.0,
242
  "repetition_rate": 0.0,
243
  "table_score": null,
244
+ "elapsed_seconds": 6.651832292001927,
245
+ "ref_length": 120,
246
+ "hyp_length": 120,
247
+ "ref_digit_count": 29,
248
  "ref_cjk_count": 0
249
  }
250
  ]
release/candidate_performance.json CHANGED
@@ -1,5 +1,6 @@
1
  {
2
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
 
3
  "image_path": "test_invoice.png",
4
  "prompt": "<image>document parsing.",
5
  "max_tokens": 256,
@@ -13,55 +14,55 @@
13
  "chip": "Apple M3 Max",
14
  "total_memory_gb": 128.0
15
  },
16
- "model_load_time_seconds": 0.8421083340072073,
17
- "memory_after_load_mb": 4008.296875,
18
  "num_warmup": 1,
19
  "num_runs": 3,
20
- "mean_tps": 297.2042493840031,
21
- "std_tps": 3.7161843770763436,
22
- "mean_elapsed_seconds": 1.2716583613364492,
23
- "mean_peak_memory_mb": 5181.5234390000005,
24
  "runs": [
25
  {
26
  "run": 1,
27
- "elapsed_seconds": 1.283014083004673,
28
  "tokens_generated": 256,
29
  "tokens_generated_source": "mlx-vlm token count",
30
- "tokens_per_second": 293.6595631095472,
31
  "tokens_per_second_source": "mlx-vlm generation_tps",
32
- "peak_memory_mb": 5181.347799,
33
  "memory_delta_mb": 1337.1867550000002,
34
- "process_peak_rss_mb": 4021.109375,
35
  "prompt_tokens": 697,
36
- "prompt_tokens_per_second": 1793.8031467011067,
37
  "finish_reason": "length"
38
  },
39
  {
40
  "run": 2,
41
- "elapsed_seconds": 1.276783292007167,
42
  "tokens_generated": 256,
43
  "tokens_generated_source": "mlx-vlm token count",
44
- "tokens_per_second": 295.6163188867776,
45
  "tokens_per_second_source": "mlx-vlm generation_tps",
46
- "peak_memory_mb": 5181.611259,
47
- "memory_delta_mb": 1337.188071,
48
- "process_peak_rss_mb": 4021.640625,
49
  "prompt_tokens": 697,
50
- "prompt_tokens_per_second": 1787.3186918039762,
51
  "finish_reason": "length"
52
  },
53
  {
54
  "run": 3,
55
- "elapsed_seconds": 1.2551777089975076,
56
  "tokens_generated": 256,
57
  "tokens_generated_source": "mlx-vlm token count",
58
- "tokens_per_second": 302.3368661556844,
59
  "tokens_per_second_source": "mlx-vlm generation_tps",
60
- "peak_memory_mb": 5181.611259,
61
- "memory_delta_mb": 1337.188071,
62
- "process_peak_rss_mb": 4021.90625,
63
  "prompt_tokens": 697,
64
- "prompt_tokens_per_second": 1798.8601642526962,
65
  "finish_reason": "length"
66
  }
67
  ]
 
1
  {
2
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
3
+ "served_revision": null,
4
  "image_path": "test_invoice.png",
5
  "prompt": "<image>document parsing.",
6
  "max_tokens": 256,
 
14
  "chip": "Apple M3 Max",
15
  "total_memory_gb": 128.0
16
  },
17
+ "model_load_time_seconds": 0.9827843750099419,
18
+ "memory_after_load_mb": 4043.265625,
19
  "num_warmup": 1,
20
  "num_runs": 3,
21
+ "mean_tps": 35.14004923388335,
22
+ "std_tps": 0.8627581629341593,
23
+ "mean_elapsed_seconds": 8.941998055335716,
24
+ "mean_peak_memory_mb": 5184.062959,
25
  "runs": [
26
  {
27
  "run": 1,
28
+ "elapsed_seconds": 8.715698832995258,
29
  "tokens_generated": 256,
30
  "tokens_generated_source": "mlx-vlm token count",
31
+ "tokens_per_second": 36.34797807809006,
32
  "tokens_per_second_source": "mlx-vlm generation_tps",
33
+ "peak_memory_mb": 5183.887319,
34
  "memory_delta_mb": 1337.1867550000002,
35
+ "process_peak_rss_mb": 4058.71875,
36
  "prompt_tokens": 697,
37
+ "prompt_tokens_per_second": 422.9274407258055,
38
  "finish_reason": "length"
39
  },
40
  {
41
  "run": 2,
42
+ "elapsed_seconds": 9.117345000006026,
43
  "tokens_generated": 256,
44
  "tokens_generated_source": "mlx-vlm token count",
45
+ "tokens_per_second": 34.685109648852176,
46
  "tokens_per_second_source": "mlx-vlm generation_tps",
47
+ "peak_memory_mb": 5184.1507790000005,
48
+ "memory_delta_mb": 1337.1880710000005,
49
+ "process_peak_rss_mb": 4065.359375,
50
  "prompt_tokens": 697,
51
+ "prompt_tokens_per_second": 413.1939079712154,
52
  "finish_reason": "length"
53
  },
54
  {
55
  "run": 3,
56
+ "elapsed_seconds": 8.992950333005865,
57
  "tokens_generated": 256,
58
  "tokens_generated_source": "mlx-vlm token count",
59
+ "tokens_per_second": 34.38705997470784,
60
  "tokens_per_second_source": "mlx-vlm generation_tps",
61
+ "peak_memory_mb": 5184.1507790000005,
62
+ "memory_delta_mb": 1337.1880710000005,
63
+ "process_peak_rss_mb": 4065.5625,
64
  "prompt_tokens": 697,
65
+ "prompt_tokens_per_second": 463.0680271067021,
66
  "finish_reason": "length"
67
  }
68
  ]
release/candidate_rswa.json CHANGED
@@ -3,23 +3,28 @@
3
  "image_path": "test_invoice.png",
4
  "prompt": "<image>document parsing.",
5
  "force_min_tokens": 8192,
 
 
 
 
 
6
  "forced_eos_token_ids": [
7
  1
8
  ],
9
  "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
10
  "processor": "arm",
11
- "baseline_memory_mb": 3844.161032,
12
  "test_results": [
13
  {
14
  "max_tokens": 512,
15
  "tokens_generated": 512,
16
  "tokens_generated_source": "mlx-vlm token count",
17
  "tokens_generated_reliable": true,
18
- "elapsed_seconds": 2.4322984160098713,
19
- "mean_tps": 264.34124967086365,
20
  "tps_source": "mlx-vlm generation_tps",
21
- "peak_memory_mb": 5181.350593,
22
- "memory_growth_mb": 1337.1895610000001,
23
  "repetition_rate": 0.0,
24
  "completed_naturally": false,
25
  "finish_reason": "length",
@@ -31,12 +36,12 @@
31
  "tokens_generated": 2048,
32
  "tokens_generated_source": "mlx-vlm token count",
33
  "tokens_generated_reliable": true,
34
- "elapsed_seconds": 8.234726583992597,
35
- "mean_tps": 261.8525825264912,
36
  "tps_source": "mlx-vlm generation_tps",
37
- "peak_memory_mb": 5181.612737,
38
- "memory_growth_mb": 1337.1895490000002,
39
- "repetition_rate": 0.09565217391304348,
40
  "completed_naturally": false,
41
  "finish_reason": "length",
42
  "forced_minimum_tokens": 2048,
@@ -47,12 +52,12 @@
47
  "tokens_generated": 8192,
48
  "tokens_generated_source": "mlx-vlm token count",
49
  "tokens_generated_reliable": true,
50
- "elapsed_seconds": 31.729143417003797,
51
- "mean_tps": 261.56365755570715,
52
  "tps_source": "mlx-vlm generation_tps",
53
- "peak_memory_mb": 5181.612737,
54
- "memory_growth_mb": 1337.1895490000002,
55
- "repetition_rate": 0.09565217391304348,
56
  "completed_naturally": false,
57
  "finish_reason": "length",
58
  "forced_minimum_tokens": 8192,
@@ -64,7 +69,7 @@
64
  "cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
65
  "tps_stable_over_length": true,
66
  "max_successful_tokens": 8192,
67
- "max_repetition_rate": 0.09565217391304348
68
  },
69
  "pass_conditions": {
70
  "cache_bounded": true,
 
3
  "image_path": "test_invoice.png",
4
  "prompt": "<image>document parsing.",
5
  "force_min_tokens": 8192,
6
+ "generation_settings": {
7
+ "temperature": 0.0,
8
+ "no_repeat_ngram_size": 35,
9
+ "ngram_window": 128
10
+ },
11
  "forced_eos_token_ids": [
12
  1
13
  ],
14
  "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
15
  "processor": "arm",
16
+ "baseline_memory_mb": 3846.700552,
17
  "test_results": [
18
  {
19
  "max_tokens": 512,
20
  "tokens_generated": 512,
21
  "tokens_generated_source": "mlx-vlm token count",
22
  "tokens_generated_reliable": true,
23
+ "elapsed_seconds": 17.311166541010607,
24
+ "mean_tps": 32.76836342370835,
25
  "tps_source": "mlx-vlm generation_tps",
26
+ "peak_memory_mb": 5183.890113,
27
+ "memory_growth_mb": 1337.1895610000006,
28
  "repetition_rate": 0.0,
29
  "completed_naturally": false,
30
  "finish_reason": "length",
 
36
  "tokens_generated": 2048,
37
  "tokens_generated_source": "mlx-vlm token count",
38
  "tokens_generated_reliable": true,
39
+ "elapsed_seconds": 65.73962916599703,
40
+ "mean_tps": 32.0896585893278,
41
  "tps_source": "mlx-vlm generation_tps",
42
+ "peak_memory_mb": 5184.152257,
43
+ "memory_growth_mb": 1337.1895489999997,
44
+ "repetition_rate": 0.20496894409937888,
45
  "completed_naturally": false,
46
  "finish_reason": "length",
47
  "forced_minimum_tokens": 2048,
 
52
  "tokens_generated": 8192,
53
  "tokens_generated_source": "mlx-vlm token count",
54
  "tokens_generated_reliable": true,
55
+ "elapsed_seconds": 268.4810879580036,
56
+ "mean_tps": 30.743604031277776,
57
  "tps_source": "mlx-vlm generation_tps",
58
+ "peak_memory_mb": 5184.152257,
59
+ "memory_growth_mb": 1337.1895489999997,
60
+ "repetition_rate": 0.20496894409937888,
61
  "completed_naturally": false,
62
  "finish_reason": "length",
63
  "forced_minimum_tokens": 8192,
 
69
  "cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
70
  "tps_stable_over_length": true,
71
  "max_successful_tokens": 8192,
72
+ "max_repetition_rate": 0.20496894409937888
73
  },
74
  "pass_conditions": {
75
  "cache_bounded": true,
release/generated_precision_map.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "vision_model": "bfloat16",
3
- "projector": "mxfp8",
4
  "language_model.model.embed_tokens": "mxfp8",
5
  "language_model.model.norm": "bfloat16",
6
  "language_model.lm_head": "affine8",
@@ -32,16 +32,16 @@
32
  "vision_encoder": {
33
  "precision": "bfloat16",
34
  "reasons": [
35
- "cer_delta=2.423686",
36
- "digit_cer_delta=1.358023",
37
  "table_score_degradation=1.000000",
38
  "protected OCR-critical group"
39
  ]
40
  },
41
  "vision_projector": {
42
- "precision": "mxfp8",
43
  "reasons": [
44
- "all measured quality deltas were non-degrading"
45
  ]
46
  },
47
  "token_embeddings": {
 
1
  {
2
  "vision_model": "bfloat16",
3
+ "projector": "bfloat16",
4
  "language_model.model.embed_tokens": "mxfp8",
5
  "language_model.model.norm": "bfloat16",
6
  "language_model.lm_head": "affine8",
 
32
  "vision_encoder": {
33
  "precision": "bfloat16",
34
  "reasons": [
35
+ "cer_delta=2.272233",
36
+ "digit_cer_delta=1.696454",
37
  "table_score_degradation=1.000000",
38
  "protected OCR-critical group"
39
  ]
40
  },
41
  "vision_projector": {
42
+ "precision": "bfloat16",
43
  "reasons": [
44
+ "protected unless all measured quality deltas are non-degrading"
45
  ]
46
  },
47
  "token_embeddings": {
release/provenance.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "created_at": "2026-07-20T23:05:17.950531+00:00",
3
  "python": "3.14.6",
4
  "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
5
  "machine": "arm64",
@@ -12,27 +12,29 @@
12
  "Pillow": "12.3.0"
13
  },
14
  "source_model": "baidu/Unlimited-OCR",
 
15
  "source_local_name": "Unlimited-OCR",
16
  "source_config": {
17
  "path": "config.json",
18
  "sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9"
19
  },
20
  "reference_model": "sahilchachra/unlimited-ocr-mxfp8-mlx",
 
21
  "target_repo": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
22
- "dataset": {
23
- "name": "eval_dataset",
24
- "num_samples": 12,
25
- "num_digit_samples": 12,
26
  "num_cjk_samples": 3,
27
  "num_table_samples": 3,
28
  "category_counts": {
29
  "cjk_document": 3,
30
- "english_document": 4,
31
  "low_quality_scan": 2,
32
  "table_numeric": 3
33
  },
34
- "manifest_sha256": "94d96ec344b02b8c4b413eafe524d6844bddcb00705af4b3fccea4c297c22509",
35
- "content_sha256": "78ca3e7c9c7584aea30b04176716a6dadf49ccf0f2bead959cbaeb9aeac68f30",
36
  "files": [
37
  {
38
  "path": "images/0001.png",
@@ -56,103 +58,220 @@
56
  },
57
  {
58
  "path": "images/0003.png",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  "size": 181040,
60
  "sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
61
  },
62
  {
63
- "path": "ground_truth/0003.txt",
64
  "size": 1333,
65
  "sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
66
  },
67
  {
68
- "path": "images/0004.png",
69
  "size": 220208,
70
  "sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
71
  },
72
  {
73
- "path": "ground_truth/0004.txt",
74
  "size": 1330,
75
  "sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
76
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  {
78
  "path": "images/0005.png",
79
- "size": 38984,
80
- "sha256": "1532b86fe28d7331379c73b62cfb0b0ae1448824566684021df262e4741b06a4"
81
  },
82
  {
83
  "path": "ground_truth/0005.txt",
84
- "size": 449,
85
- "sha256": "fb8ebea089dacff057d98feb6450436f3f6db65f0bac29513d3417e94451686e"
86
  },
87
  {
88
  "path": "images/0006.png",
89
- "size": 38512,
90
- "sha256": "8c1bf51f86f0f22c8898cec8c77049b6f023b77aab77f55be40f75b477ab6383"
91
  },
92
  {
93
  "path": "ground_truth/0006.txt",
94
- "size": 449,
95
- "sha256": "f60ff098ac431b1021fb85f810dd6264c1c021fab0810ab97ce304c5eb802598"
96
  },
97
  {
98
  "path": "images/0007.png",
99
- "size": 38911,
100
- "sha256": "a87e7f53196e9d3f72af611394297ec64e9eb222d2872b62f0c5a2014d83ecfc"
101
  },
102
  {
103
  "path": "ground_truth/0007.txt",
104
- "size": 449,
105
- "sha256": "0381ec16f8895290dfc2b81f97933f5568a68bfe731cc4791272ff8269e57c0e"
106
  },
107
  {
108
  "path": "images/0008.png",
109
- "size": 45665,
110
- "sha256": "d9ff0cc9ffa639e485b0d445e1f4856f479ed9e2e485b1ca1ece9a9a043bc6c8"
111
  },
112
  {
113
  "path": "ground_truth/0008.txt",
114
- "size": 185,
115
- "sha256": "2dbeb40c0172dc618c7298a87e131153f51682dcaa33efefa67b7e50ecd7a8d1"
116
  },
117
  {
118
  "path": "images/0009.png",
119
- "size": 40784,
120
- "sha256": "f93ea216d77bfdb23f6c8c113e33c946318d6eac6ef3bdaf2e2f2c0d86c154c9"
121
  },
122
  {
123
  "path": "ground_truth/0009.txt",
124
- "size": 188,
125
- "sha256": "957232168770c2a15efeb33126a7d2d8141ce34782106b979e4b31acfa4f9bee"
126
  },
127
  {
128
  "path": "images/0010.png",
129
- "size": 20512,
130
- "sha256": "5ff52fd340da20784c4c934cbac3ae9a0788b7d1654d71934100624842ddf764"
131
  },
132
  {
133
  "path": "ground_truth/0010.txt",
134
- "size": 194,
135
- "sha256": "2f9054687e5eb369e35d985bc2fe9d2d37ffeb8cafe204c3e81830f4431f5793"
136
  },
137
  {
138
  "path": "images/0011.png",
139
- "size": 486656,
140
- "sha256": "74e878d54295c224481f0f1b51a670459b855b16492c246c27636d71f4666e68"
141
  },
142
  {
143
  "path": "ground_truth/0011.txt",
144
- "size": 119,
145
- "sha256": "8f8bc16447a12c17e4036c4c5fd936faef227310a564de983ebe2f4f22742d83"
146
  },
147
  {
148
  "path": "images/0012.png",
149
- "size": 515472,
150
- "sha256": "9a6e49af19e893000b8e96bece22fe99fedd3741e7ac3dede04a112ea77b5b5c"
151
  },
152
  {
153
  "path": "ground_truth/0012.txt",
154
- "size": 119,
155
- "sha256": "1ccf00fe53da00ba3a2340dbe95e1692e87efccdc04b191c0f1d52dab23169a7"
156
  }
157
  ]
158
  },
 
1
  {
2
+ "created_at": "2026-07-21T03:08:54.605846+00:00",
3
  "python": "3.14.6",
4
  "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
5
  "machine": "arm64",
 
12
  "Pillow": "12.3.0"
13
  },
14
  "source_model": "baidu/Unlimited-OCR",
15
+ "source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
16
  "source_local_name": "Unlimited-OCR",
17
  "source_config": {
18
  "path": "config.json",
19
  "sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9"
20
  },
21
  "reference_model": "sahilchachra/unlimited-ocr-mxfp8-mlx",
22
+ "reference_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
23
  "target_repo": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
24
+ "calibration_dataset": {
25
+ "name": "calibration",
26
+ "num_samples": 10,
27
+ "num_digit_samples": 10,
28
  "num_cjk_samples": 3,
29
  "num_table_samples": 3,
30
  "category_counts": {
31
  "cjk_document": 3,
32
+ "english_document": 2,
33
  "low_quality_scan": 2,
34
  "table_numeric": 3
35
  },
36
+ "manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
37
+ "content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
38
  "files": [
39
  {
40
  "path": "images/0001.png",
 
58
  },
59
  {
60
  "path": "images/0003.png",
61
+ "size": 40039,
62
+ "sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
63
+ },
64
+ {
65
+ "path": "ground_truth/0003.txt",
66
+ "size": 459,
67
+ "sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
68
+ },
69
+ {
70
+ "path": "images/0004.png",
71
+ "size": 40356,
72
+ "sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
73
+ },
74
+ {
75
+ "path": "ground_truth/0004.txt",
76
+ "size": 459,
77
+ "sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
78
+ },
79
+ {
80
+ "path": "images/0005.png",
81
+ "size": 40650,
82
+ "sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
83
+ },
84
+ {
85
+ "path": "ground_truth/0005.txt",
86
+ "size": 459,
87
+ "sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
88
+ },
89
+ {
90
+ "path": "images/0006.png",
91
+ "size": 46036,
92
+ "sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
93
+ },
94
+ {
95
+ "path": "ground_truth/0006.txt",
96
+ "size": 191,
97
+ "sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
98
+ },
99
+ {
100
+ "path": "images/0007.png",
101
+ "size": 41828,
102
+ "sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
103
+ },
104
+ {
105
+ "path": "ground_truth/0007.txt",
106
+ "size": 195,
107
+ "sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
108
+ },
109
+ {
110
+ "path": "images/0008.png",
111
+ "size": 20975,
112
+ "sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
113
+ },
114
+ {
115
+ "path": "ground_truth/0008.txt",
116
+ "size": 201,
117
+ "sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
118
+ },
119
+ {
120
+ "path": "images/0009.png",
121
+ "size": 487313,
122
+ "sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
123
+ },
124
+ {
125
+ "path": "ground_truth/0009.txt",
126
+ "size": 121,
127
+ "sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
128
+ },
129
+ {
130
+ "path": "images/0010.png",
131
+ "size": 515820,
132
+ "sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
133
+ },
134
+ {
135
+ "path": "ground_truth/0010.txt",
136
+ "size": 121,
137
+ "sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
138
+ }
139
+ ]
140
+ },
141
+ "evaluation_dataset": {
142
+ "name": "evaluation",
143
+ "num_samples": 12,
144
+ "num_digit_samples": 12,
145
+ "num_cjk_samples": 3,
146
+ "num_table_samples": 3,
147
+ "category_counts": {
148
+ "cjk_document": 3,
149
+ "english_document": 4,
150
+ "low_quality_scan": 2,
151
+ "table_numeric": 3
152
+ },
153
+ "manifest_sha256": "111646a6f59ed32689fbc658c1cb4ed60629e1bbd909d6137df469e7b489f121",
154
+ "content_sha256": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
155
+ "files": [
156
+ {
157
+ "path": "images/0001.png",
158
  "size": 181040,
159
  "sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
160
  },
161
  {
162
+ "path": "ground_truth/0001.txt",
163
  "size": 1333,
164
  "sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
165
  },
166
  {
167
+ "path": "images/0002.png",
168
  "size": 220208,
169
  "sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
170
  },
171
  {
172
+ "path": "ground_truth/0002.txt",
173
  "size": 1330,
174
  "sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
175
  },
176
+ {
177
+ "path": "images/0003.png",
178
+ "size": 119613,
179
+ "sha256": "bcf22c94459b970f1ee11088d8a902dbc56975e487c489b4714dedf8f3a4dca1"
180
+ },
181
+ {
182
+ "path": "ground_truth/0003.txt",
183
+ "size": 637,
184
+ "sha256": "a035ed288c8cc0e73e6cddc08c804be9b1ed32eee98e7f85c0e027bd88221b66"
185
+ },
186
+ {
187
+ "path": "images/0004.png",
188
+ "size": 67701,
189
+ "sha256": "baea8365beb81cc9939e59d3c8a0b88ab17999122eb1729e33214450a4fba987"
190
+ },
191
+ {
192
+ "path": "ground_truth/0004.txt",
193
+ "size": 478,
194
+ "sha256": "af1c29967e55595362ab41e25000e00167474cbe38ea43b8816f1f1395ef6e72"
195
+ },
196
  {
197
  "path": "images/0005.png",
198
+ "size": 41300,
199
+ "sha256": "65662634fc3b5df7d006f40d276fb2869809a86a2027f464c1d6776094e16f16"
200
  },
201
  {
202
  "path": "ground_truth/0005.txt",
203
+ "size": 459,
204
+ "sha256": "1d565459641aca5056fdcccabe2cdc6834107bef219645a54cd28db2e06b05ea"
205
  },
206
  {
207
  "path": "images/0006.png",
208
+ "size": 40574,
209
+ "sha256": "67dbd4d07cb6e3f971937dc93a35ac81775580ee4d8895373afe6ae7e6b328dd"
210
  },
211
  {
212
  "path": "ground_truth/0006.txt",
213
+ "size": 459,
214
+ "sha256": "0e134815b0bee585e3841d5a22a296c136fb43f00a9e3d48d98d2df24e884b08"
215
  },
216
  {
217
  "path": "images/0007.png",
218
+ "size": 41027,
219
+ "sha256": "2b54b5222174bf4d41d92a6ee97aa1b2a3e57daca987b55906793df807dfe08f"
220
  },
221
  {
222
  "path": "ground_truth/0007.txt",
223
+ "size": 459,
224
+ "sha256": "b63a7c471f4d36cc8bafddcbd7b4a8d92d6f92e73ab1e260505c2413d6e39146"
225
  },
226
  {
227
  "path": "images/0008.png",
228
+ "size": 46025,
229
+ "sha256": "c8c8ed310cbdebf7e27adf3361e6ce5939922bd674af12205cea441a9b53399d"
230
  },
231
  {
232
  "path": "ground_truth/0008.txt",
233
+ "size": 191,
234
+ "sha256": "246e79c3ef9faa52dd10c400eecf594314493c88ae5f7794a6129dd90e850107"
235
  },
236
  {
237
  "path": "images/0009.png",
238
+ "size": 41617,
239
+ "sha256": "a00eaebf40f9369b6851a949a0aedc82a57cf1104a957e42880b1f3aa65606f0"
240
  },
241
  {
242
  "path": "ground_truth/0009.txt",
243
+ "size": 195,
244
+ "sha256": "5653ef6df8a330496e73c6ddd71c9b2280818e13b11b6ff8842622ad53d24a5b"
245
  },
246
  {
247
  "path": "images/0010.png",
248
+ "size": 21089,
249
+ "sha256": "59b6edec6b2ef0369f1d7fbe9fe96648fda2982db0eae377199d5edf26855900"
250
  },
251
  {
252
  "path": "ground_truth/0010.txt",
253
+ "size": 201,
254
+ "sha256": "6d2f6474c94dea173288aebde046b6db40534548e8187cb776dc494abaa04ce2"
255
  },
256
  {
257
  "path": "images/0011.png",
258
+ "size": 487332,
259
+ "sha256": "70f57ebf6f41479e443e4356422e6dd619ffdcf2f0df7837943f5427545cd266"
260
  },
261
  {
262
  "path": "ground_truth/0011.txt",
263
+ "size": 121,
264
+ "sha256": "f550334b0b7d3508ef898d0e144593c6d005dd32d1bbcb837aeb650da56e9eb8"
265
  },
266
  {
267
  "path": "images/0012.png",
268
+ "size": 516088,
269
+ "sha256": "fdd426f69928ba7bcc6008655fa0dbd950b6844eba8322491e7a5399bf052d59"
270
  },
271
  {
272
  "path": "ground_truth/0012.txt",
273
+ "size": 121,
274
+ "sha256": "d040adc39553bc337df562486b3b5148727660214ec79a3af48b9cf5a2bc23f7"
275
  }
276
  ]
277
  },
release/reference_accuracy.json CHANGED
@@ -1,5 +1,7 @@
1
  {
2
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
 
 
3
  "prompt": "<image>document parsing.",
4
  "max_tokens": 1024,
5
  "profile": "accurate",
@@ -12,183 +14,237 @@
12
  },
13
  "num_images": 12,
14
  "num_samples": 12,
15
- "total_time_seconds": 26.09562641500088,
16
- "mean_cer": 1.057248773567081,
17
- "mean_wer": 0.9346679608165291,
18
  "num_digit_samples": 12,
19
  "num_cjk_samples": 3,
20
- "mean_digit_cer": 0.20915311607827292,
21
  "mean_cjk_cer": 1.0,
22
  "num_table_samples": 3,
23
  "mean_table_score": 1.0,
24
  "mean_repetition_rate": 0.00026371308016877635,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  "per_file": [
26
  {
27
  "file": "0001.png",
28
- "cer": 0.06144256455921639,
29
- "wer": 0.20179372197309417,
30
- "digit_cer": 0.3023255813953488,
 
31
  "cjk_cer": 0.0,
32
- "repetition_rate": 0.0,
33
  "table_score": null,
34
- "elapsed_seconds": 3.459972665994428,
35
- "ref_length": 1345,
36
- "hyp_length": 1382,
37
- "ref_digit_count": 86,
38
  "ref_cjk_count": 0
39
  },
40
  {
41
  "file": "0002.png",
42
- "cer": 0.47234678624813153,
43
- "wer": 0.5988023952095808,
44
- "digit_cer": 0.5578947368421052,
 
45
  "cjk_cer": 0.0,
46
  "repetition_rate": 0.0,
47
  "table_score": null,
48
- "elapsed_seconds": 4.406020459005958,
49
- "ref_length": 835,
50
- "hyp_length": 771,
51
- "ref_digit_count": 95,
52
  "ref_cjk_count": 0
53
  },
54
  {
55
  "file": "0003.png",
56
- "cer": 1.1751563896336015,
57
- "wer": 1.439252336448598,
58
- "digit_cer": 0.48148148148148145,
 
59
  "cjk_cer": 0.0,
60
- "repetition_rate": 0.0031645569620253164,
61
  "table_score": null,
62
- "elapsed_seconds": 4.409982333003427,
63
- "ref_length": 1332,
64
- "hyp_length": 2457,
65
- "ref_digit_count": 27,
66
  "ref_cjk_count": 0
67
  },
68
  {
69
  "file": "0004.png",
70
- "cer": 0.7319494584837545,
71
- "wer": 0.6216216216216216,
72
- "digit_cer": 0.288135593220339,
 
73
  "cjk_cer": 0.0,
74
  "repetition_rate": 0.0,
75
  "table_score": null,
76
- "elapsed_seconds": 4.450652749990695,
77
- "ref_length": 1329,
78
- "hyp_length": 1794,
79
- "ref_digit_count": 59,
80
  "ref_cjk_count": 0
81
  },
82
  {
83
  "file": "0005.png",
84
- "cer": 0.17715617715617715,
 
85
  "wer": 1.5,
86
- "digit_cer": 0.2,
87
  "cjk_cer": 0.0,
88
  "repetition_rate": 0.0,
89
  "table_score": 1.0,
90
- "elapsed_seconds": 1.2356766660086578,
91
- "ref_length": 436,
92
- "hyp_length": 508,
93
- "ref_digit_count": 70,
94
  "ref_cjk_count": 0
95
  },
96
  {
97
  "file": "0006.png",
98
- "cer": 0.17715617715617715,
 
99
  "wer": 1.5,
100
- "digit_cer": 0.2,
101
  "cjk_cer": 0.0,
102
  "repetition_rate": 0.0,
103
  "table_score": 1.0,
104
- "elapsed_seconds": 1.2516661249974277,
105
- "ref_length": 436,
106
- "hyp_length": 508,
107
- "ref_digit_count": 70,
108
  "ref_cjk_count": 0
109
  },
110
  {
111
  "file": "0007.png",
112
- "cer": 0.17715617715617715,
 
113
  "wer": 1.5,
114
- "digit_cer": 0.2,
115
  "cjk_cer": 0.0,
116
  "repetition_rate": 0.0,
117
  "table_score": 1.0,
118
- "elapsed_seconds": 1.274972874991363,
119
- "ref_length": 436,
120
- "hyp_length": 508,
121
- "ref_digit_count": 70,
122
  "ref_cjk_count": 0
123
  },
124
  {
125
  "file": "0008.png",
126
- "cer": 1.6867469879518073,
 
127
  "wer": 0.9,
128
  "digit_cer": 0.0,
129
  "cjk_cer": 1.0,
130
  "repetition_rate": 0.0,
131
  "table_score": null,
132
- "elapsed_seconds": 1.1017809159966419,
133
- "ref_length": 92,
134
- "hyp_length": 187,
135
- "ref_digit_count": 26,
136
  "ref_cjk_count": 41
137
  },
138
  {
139
  "file": "0009.png",
140
- "cer": 5.670731707317073,
 
141
  "wer": 1.0,
142
  "digit_cer": 0.0,
143
  "cjk_cer": 1.0,
144
  "repetition_rate": 0.0,
145
  "table_score": null,
146
- "elapsed_seconds": 1.35718295800325,
147
- "ref_length": 93,
148
- "hyp_length": 509,
149
- "ref_digit_count": 25,
150
  "ref_cjk_count": 42
151
  },
152
  {
153
  "file": "0010.png",
154
- "cer": 2.357142857142857,
 
155
  "wer": 1.9545454545454546,
156
- "digit_cer": 0.28,
157
  "cjk_cer": 1.0,
158
  "repetition_rate": 0.0,
159
  "table_score": null,
160
- "elapsed_seconds": 1.1220285840099677,
161
- "ref_length": 105,
162
- "hyp_length": 269,
163
- "ref_digit_count": 25,
164
  "ref_cjk_count": 44
165
  },
166
  {
167
  "file": "0011.png",
168
- "cer": 0.0,
169
- "wer": 0.0,
170
- "digit_cer": 0.0,
 
171
  "cjk_cer": 0.0,
172
  "repetition_rate": 0.0,
173
  "table_score": null,
174
- "elapsed_seconds": 1.0181266669969773,
175
- "ref_length": 118,
176
- "hyp_length": 118,
177
- "ref_digit_count": 27,
178
  "ref_cjk_count": 0
179
  },
180
  {
181
  "file": "0012.png",
182
- "cer": 0.0,
183
- "wer": 0.0,
 
184
  "digit_cer": 0.0,
185
  "cjk_cer": 0.0,
186
  "repetition_rate": 0.0,
187
  "table_score": null,
188
- "elapsed_seconds": 1.0075634160020854,
189
- "ref_length": 118,
190
- "hyp_length": 118,
191
- "ref_digit_count": 27,
192
  "ref_cjk_count": 0
193
  }
194
  ]
 
1
  {
2
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
3
+ "served_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
4
+ "backend": "mlx",
5
  "prompt": "<image>document parsing.",
6
  "max_tokens": 1024,
7
  "profile": "accurate",
 
14
  },
15
  "num_images": 12,
16
  "num_samples": 12,
17
+ "total_time_seconds": 162.6677352060069,
18
+ "mean_cer": 0.9073812810686629,
19
+ "mean_wer": 1.001372473311303,
20
  "num_digit_samples": 12,
21
  "num_cjk_samples": 3,
22
+ "mean_digit_cer": 0.4065110611974061,
23
  "mean_cjk_cer": 1.0,
24
  "num_table_samples": 3,
25
  "mean_table_score": 1.0,
26
  "mean_repetition_rate": 0.00026371308016877635,
27
+ "by_category": {
28
+ "cjk_document": {
29
+ "num_samples": 3,
30
+ "mean_cer": 2.317035024900193,
31
+ "mean_digit_cer": 0.08888888888888889,
32
+ "mean_cjk_cer": 1.0,
33
+ "mean_table_score": null,
34
+ "num_digit_samples": 3,
35
+ "num_cjk_samples": 3,
36
+ "num_table_samples": 0
37
+ },
38
+ "english_document": {
39
+ "num_samples": 4,
40
+ "mean_cer": 0.8171043138735056,
41
+ "mean_digit_cer": 0.9432759996841724,
42
+ "mean_cjk_cer": null,
43
+ "mean_table_score": null,
44
+ "num_digit_samples": 4,
45
+ "num_cjk_samples": 0,
46
+ "num_table_samples": 0
47
+ },
48
+ "low_quality_scan": {
49
+ "num_samples": 2,
50
+ "mean_cer": 0.07142857142857142,
51
+ "mean_digit_cer": 0.13793103448275862,
52
+ "mean_cjk_cer": null,
53
+ "mean_table_score": null,
54
+ "num_digit_samples": 2,
55
+ "num_cjk_samples": 0,
56
+ "num_table_samples": 0
57
+ },
58
+ "table_numeric": {
59
+ "num_samples": 3,
60
+ "mean_cer": 0.1753986332574032,
61
+ "mean_digit_cer": 0.1875,
62
+ "mean_cjk_cer": null,
63
+ "mean_table_score": 1.0,
64
+ "num_digit_samples": 3,
65
+ "num_cjk_samples": 0,
66
+ "num_table_samples": 3
67
+ }
68
+ },
69
  "per_file": [
70
  {
71
  "file": "0001.png",
72
+ "category": "english_document",
73
+ "cer": 1.1751563896336015,
74
+ "wer": 1.439252336448598,
75
+ "digit_cer": 0.48148148148148145,
76
  "cjk_cer": 0.0,
77
+ "repetition_rate": 0.0031645569620253164,
78
  "table_score": null,
79
+ "elapsed_seconds": 32.467281207995256,
80
+ "ref_length": 1332,
81
+ "hyp_length": 2457,
82
+ "ref_digit_count": 27,
83
  "ref_cjk_count": 0
84
  },
85
  {
86
  "file": "0002.png",
87
+ "category": "english_document",
88
+ "cer": 0.7319494584837545,
89
+ "wer": 0.6216216216216216,
90
+ "digit_cer": 0.288135593220339,
91
  "cjk_cer": 0.0,
92
  "repetition_rate": 0.0,
93
  "table_score": null,
94
+ "elapsed_seconds": 31.99284904099477,
95
+ "ref_length": 1329,
96
+ "hyp_length": 1794,
97
+ "ref_digit_count": 59,
98
  "ref_cjk_count": 0
99
  },
100
  {
101
  "file": "0003.png",
102
+ "category": "english_document",
103
+ "cer": 1.2802303262955854,
104
+ "wer": 1.2586206896551724,
105
+ "digit_cer": 2.767123287671233,
106
  "cjk_cer": 0.0,
107
+ "repetition_rate": 0.0,
108
  "table_score": null,
109
+ "elapsed_seconds": 30.016402541004936,
110
+ "ref_length": 636,
111
+ "hyp_length": 1349,
112
+ "ref_digit_count": 73,
113
  "ref_cjk_count": 0
114
  },
115
  {
116
  "file": "0004.png",
117
+ "category": "english_document",
118
+ "cer": 0.08108108108108109,
119
+ "wer": 0.15492957746478872,
120
+ "digit_cer": 0.23636363636363636,
121
  "cjk_cer": 0.0,
122
  "repetition_rate": 0.0,
123
  "table_score": null,
124
+ "elapsed_seconds": 12.61737366700254,
125
+ "ref_length": 477,
126
+ "hyp_length": 478,
127
+ "ref_digit_count": 55,
128
  "ref_cjk_count": 0
129
  },
130
  {
131
  "file": "0005.png",
132
+ "category": "table_numeric",
133
+ "cer": 0.17539863325740318,
134
  "wer": 1.5,
135
+ "digit_cer": 0.1875,
136
  "cjk_cer": 0.0,
137
  "repetition_rate": 0.0,
138
  "table_score": 1.0,
139
+ "elapsed_seconds": 8.039810500005842,
140
+ "ref_length": 446,
141
+ "hyp_length": 519,
142
+ "ref_digit_count": 80,
143
  "ref_cjk_count": 0
144
  },
145
  {
146
  "file": "0006.png",
147
+ "category": "table_numeric",
148
+ "cer": 0.17539863325740318,
149
  "wer": 1.5,
150
+ "digit_cer": 0.1875,
151
  "cjk_cer": 0.0,
152
  "repetition_rate": 0.0,
153
  "table_score": 1.0,
154
+ "elapsed_seconds": 8.472794083005283,
155
+ "ref_length": 446,
156
+ "hyp_length": 519,
157
+ "ref_digit_count": 80,
158
  "ref_cjk_count": 0
159
  },
160
  {
161
  "file": "0007.png",
162
+ "category": "table_numeric",
163
+ "cer": 0.17539863325740318,
164
  "wer": 1.5,
165
+ "digit_cer": 0.1875,
166
  "cjk_cer": 0.0,
167
  "repetition_rate": 0.0,
168
  "table_score": 1.0,
169
+ "elapsed_seconds": 7.832311457998003,
170
+ "ref_length": 446,
171
+ "hyp_length": 519,
172
+ "ref_digit_count": 80,
173
  "ref_cjk_count": 0
174
  },
175
  {
176
  "file": "0008.png",
177
+ "category": "cjk_document",
178
+ "cer": 1.5730337078651686,
179
  "wer": 0.9,
180
  "digit_cer": 0.0,
181
  "cjk_cer": 1.0,
182
  "repetition_rate": 0.0,
183
  "table_score": null,
184
+ "elapsed_seconds": 6.520762083993759,
185
+ "ref_length": 98,
186
+ "hyp_length": 193,
187
+ "ref_digit_count": 30,
188
  "ref_cjk_count": 41
189
  },
190
  {
191
  "file": "0009.png",
192
+ "category": "cjk_document",
193
+ "cer": 3.202247191011236,
194
  "wer": 1.0,
195
  "digit_cer": 0.0,
196
  "cjk_cer": 1.0,
197
  "repetition_rate": 0.0,
198
  "table_score": null,
199
+ "elapsed_seconds": 6.874465999993845,
200
+ "ref_length": 100,
201
+ "hyp_length": 334,
202
+ "ref_digit_count": 30,
203
  "ref_cjk_count": 42
204
  },
205
  {
206
  "file": "0010.png",
207
+ "category": "cjk_document",
208
+ "cer": 2.1758241758241756,
209
  "wer": 1.9545454545454546,
210
+ "digit_cer": 0.26666666666666666,
211
  "cjk_cer": 1.0,
212
  "repetition_rate": 0.0,
213
  "table_score": null,
214
+ "elapsed_seconds": 6.385247083002469,
215
+ "ref_length": 112,
216
+ "hyp_length": 275,
217
+ "ref_digit_count": 30,
218
  "ref_cjk_count": 44
219
  },
220
  {
221
  "file": "0011.png",
222
+ "category": "low_quality_scan",
223
+ "cer": 0.12380952380952381,
224
+ "wer": 0.125,
225
+ "digit_cer": 0.27586206896551724,
226
  "cjk_cer": 0.0,
227
  "repetition_rate": 0.0,
228
  "table_score": null,
229
+ "elapsed_seconds": 5.718108458007919,
230
+ "ref_length": 120,
231
+ "hyp_length": 135,
232
+ "ref_digit_count": 29,
233
  "ref_cjk_count": 0
234
  },
235
  {
236
  "file": "0012.png",
237
+ "category": "low_quality_scan",
238
+ "cer": 0.01904761904761905,
239
+ "wer": 0.0625,
240
  "digit_cer": 0.0,
241
  "cjk_cer": 0.0,
242
  "repetition_rate": 0.0,
243
  "table_score": null,
244
+ "elapsed_seconds": 5.730329083002289,
245
+ "ref_length": 120,
246
+ "hyp_length": 123,
247
+ "ref_digit_count": 29,
248
  "ref_cjk_count": 0
249
  }
250
  ]
release/reference_performance.json CHANGED
@@ -1,5 +1,6 @@
1
  {
2
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
 
3
  "image_path": "test_invoice.png",
4
  "prompt": "<image>document parsing.",
5
  "max_tokens": 256,
@@ -13,55 +14,55 @@
13
  "chip": "Apple M3 Max",
14
  "total_memory_gb": 128.0
15
  },
16
- "model_load_time_seconds": 5.561058167004376,
17
- "memory_after_load_mb": 3994.34375,
18
  "num_warmup": 1,
19
  "num_runs": 3,
20
- "mean_tps": 312.7284719764298,
21
- "std_tps": 0.9770038029028864,
22
- "mean_elapsed_seconds": 1.2253679166703175,
23
  "mean_peak_memory_mb": 5165.991419999999,
24
  "runs": [
25
  {
26
  "run": 1,
27
- "elapsed_seconds": 1.226103625012911,
28
  "tokens_generated": 256,
29
  "tokens_generated_source": "mlx-vlm token count",
30
- "tokens_per_second": 311.86183520486986,
31
  "tokens_per_second_source": "mlx-vlm generation_tps",
32
  "peak_memory_mb": 5165.81578,
33
  "memory_delta_mb": 1337.186768,
34
- "process_peak_rss_mb": 4015.96875,
35
  "prompt_tokens": 697,
36
- "prompt_tokens_per_second": 1816.7391562081139,
37
  "finish_reason": "length"
38
  },
39
  {
40
  "run": 2,
41
- "elapsed_seconds": 1.2276696659973823,
42
  "tokens_generated": 256,
43
  "tokens_generated_source": "mlx-vlm token count",
44
- "tokens_per_second": 312.2298514286067,
45
  "tokens_per_second_source": "mlx-vlm generation_tps",
46
  "peak_memory_mb": 5166.07924,
47
  "memory_delta_mb": 1337.188084,
48
- "process_peak_rss_mb": 4022.953125,
49
  "prompt_tokens": 697,
50
- "prompt_tokens_per_second": 1801.9665867511062,
51
  "finish_reason": "length"
52
  },
53
  {
54
  "run": 3,
55
- "elapsed_seconds": 1.2223304590006592,
56
  "tokens_generated": 256,
57
  "tokens_generated_source": "mlx-vlm token count",
58
- "tokens_per_second": 314.0937292958128,
59
  "tokens_per_second_source": "mlx-vlm generation_tps",
60
  "peak_memory_mb": 5166.07924,
61
  "memory_delta_mb": 1337.188084,
62
- "process_peak_rss_mb": 4023.265625,
63
  "prompt_tokens": 697,
64
- "prompt_tokens_per_second": 1800.5791804452258,
65
  "finish_reason": "length"
66
  }
67
  ]
 
1
  {
2
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
3
+ "served_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
4
  "image_path": "test_invoice.png",
5
  "prompt": "<image>document parsing.",
6
  "max_tokens": 256,
 
14
  "chip": "Apple M3 Max",
15
  "total_memory_gb": 128.0
16
  },
17
+ "model_load_time_seconds": 2.9305669579916866,
18
+ "memory_after_load_mb": 3992.859375,
19
  "num_warmup": 1,
20
  "num_runs": 3,
21
+ "mean_tps": 35.07146198590831,
22
+ "std_tps": 0.290807343078657,
23
+ "mean_elapsed_seconds": 8.953089208662277,
24
  "mean_peak_memory_mb": 5165.991419999999,
25
  "runs": [
26
  {
27
  "run": 1,
28
+ "elapsed_seconds": 9.042429541994352,
29
  "tokens_generated": 256,
30
  "tokens_generated_source": "mlx-vlm token count",
31
+ "tokens_per_second": 34.74497597308309,
32
  "tokens_per_second_source": "mlx-vlm generation_tps",
33
  "peak_memory_mb": 5165.81578,
34
  "memory_delta_mb": 1337.186768,
35
+ "process_peak_rss_mb": 4022.6875,
36
  "prompt_tokens": 697,
37
+ "prompt_tokens_per_second": 429.97806661114964,
38
  "finish_reason": "length"
39
  },
40
  {
41
  "run": 2,
42
+ "elapsed_seconds": 9.023512416999438,
43
  "tokens_generated": 256,
44
  "tokens_generated_source": "mlx-vlm token count",
45
+ "tokens_per_second": 35.01812122387363,
46
  "tokens_per_second_source": "mlx-vlm generation_tps",
47
  "peak_memory_mb": 5166.07924,
48
  "memory_delta_mb": 1337.188084,
49
+ "process_peak_rss_mb": 4023.046875,
50
  "prompt_tokens": 697,
51
+ "prompt_tokens_per_second": 417.40764558358615,
52
  "finish_reason": "length"
53
  },
54
  {
55
  "run": 3,
56
+ "elapsed_seconds": 8.793325666993042,
57
  "tokens_generated": 256,
58
  "tokens_generated_source": "mlx-vlm token count",
59
+ "tokens_per_second": 35.451288760768186,
60
  "tokens_per_second_source": "mlx-vlm generation_tps",
61
  "peak_memory_mb": 5166.07924,
62
  "memory_delta_mb": 1337.188084,
63
+ "process_peak_rss_mb": 4023.3125,
64
  "prompt_tokens": 697,
65
+ "prompt_tokens_per_second": 449.69288066410945,
66
  "finish_reason": "length"
67
  }
68
  ]
release/release_manifest.json CHANGED
@@ -1,10 +1,11 @@
1
  {
2
- "schema_version": 1,
3
- "created_at": "2026-07-20T23:31:41.909496+00:00",
4
  "release_approved": true,
5
  "repo_id": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
6
  "source": {
7
  "id": "baidu/Unlimited-OCR",
 
8
  "weights": {
9
  "files": [
10
  {
@@ -20,6 +21,7 @@
20
  },
21
  "reference": {
22
  "id": "sahilchachra/unlimited-ocr-mxfp8-mlx",
 
23
  "weights": {
24
  "files": [
25
  {
@@ -39,13 +41,63 @@
39
  "files": [
40
  {
41
  "name": "model.safetensors",
42
- "size": 3843160559,
43
- "sha256": "27568517565832e42daa563f6939e66a8ebb50cce0b01a24ecebb3ea6b4d0b10"
44
  }
45
  ],
46
- "total_size_bytes": 3843160559,
47
- "total_size_gb": 3.5792221864685416,
48
- "aggregate_sha256": "a4f6e9934f90564906f42929542ad68493f0c6187ad431128e447debbf52b6f4"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  },
50
  "metadata": {
51
  "passed": true,
@@ -55,25 +107,210 @@
55
  "mxfp8": true,
56
  "sliding_window": true,
57
  "processor_class": true,
58
- "sft_format": true
 
 
 
59
  },
60
- "sliding_window": 128
 
 
61
  }
62
  },
63
- "dataset": {
64
- "name": "eval_dataset",
65
- "num_samples": 12,
66
- "num_digit_samples": 12,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  "num_cjk_samples": 3,
68
  "num_table_samples": 3,
69
  "category_counts": {
70
  "cjk_document": 3,
71
- "english_document": 4,
72
  "low_quality_scan": 2,
73
  "table_numeric": 3
74
  },
75
- "manifest_sha256": "94d96ec344b02b8c4b413eafe524d6844bddcb00705af4b3fccea4c297c22509",
76
- "content_sha256": "78ca3e7c9c7584aea30b04176716a6dadf49ccf0f2bead959cbaeb9aeac68f30",
77
  "files": [
78
  {
79
  "path": "images/0001.png",
@@ -97,120 +334,239 @@
97
  },
98
  {
99
  "path": "images/0003.png",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  "size": 181040,
101
  "sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
102
  },
103
  {
104
- "path": "ground_truth/0003.txt",
105
  "size": 1333,
106
  "sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
107
  },
108
  {
109
- "path": "images/0004.png",
110
  "size": 220208,
111
  "sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
112
  },
113
  {
114
- "path": "ground_truth/0004.txt",
115
  "size": 1330,
116
  "sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
117
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  {
119
  "path": "images/0005.png",
120
- "size": 38984,
121
- "sha256": "1532b86fe28d7331379c73b62cfb0b0ae1448824566684021df262e4741b06a4"
122
  },
123
  {
124
  "path": "ground_truth/0005.txt",
125
- "size": 449,
126
- "sha256": "fb8ebea089dacff057d98feb6450436f3f6db65f0bac29513d3417e94451686e"
127
  },
128
  {
129
  "path": "images/0006.png",
130
- "size": 38512,
131
- "sha256": "8c1bf51f86f0f22c8898cec8c77049b6f023b77aab77f55be40f75b477ab6383"
132
  },
133
  {
134
  "path": "ground_truth/0006.txt",
135
- "size": 449,
136
- "sha256": "f60ff098ac431b1021fb85f810dd6264c1c021fab0810ab97ce304c5eb802598"
137
  },
138
  {
139
  "path": "images/0007.png",
140
- "size": 38911,
141
- "sha256": "a87e7f53196e9d3f72af611394297ec64e9eb222d2872b62f0c5a2014d83ecfc"
142
  },
143
  {
144
  "path": "ground_truth/0007.txt",
145
- "size": 449,
146
- "sha256": "0381ec16f8895290dfc2b81f97933f5568a68bfe731cc4791272ff8269e57c0e"
147
  },
148
  {
149
  "path": "images/0008.png",
150
- "size": 45665,
151
- "sha256": "d9ff0cc9ffa639e485b0d445e1f4856f479ed9e2e485b1ca1ece9a9a043bc6c8"
152
  },
153
  {
154
  "path": "ground_truth/0008.txt",
155
- "size": 185,
156
- "sha256": "2dbeb40c0172dc618c7298a87e131153f51682dcaa33efefa67b7e50ecd7a8d1"
157
  },
158
  {
159
  "path": "images/0009.png",
160
- "size": 40784,
161
- "sha256": "f93ea216d77bfdb23f6c8c113e33c946318d6eac6ef3bdaf2e2f2c0d86c154c9"
162
  },
163
  {
164
  "path": "ground_truth/0009.txt",
165
- "size": 188,
166
- "sha256": "957232168770c2a15efeb33126a7d2d8141ce34782106b979e4b31acfa4f9bee"
167
  },
168
  {
169
  "path": "images/0010.png",
170
- "size": 20512,
171
- "sha256": "5ff52fd340da20784c4c934cbac3ae9a0788b7d1654d71934100624842ddf764"
172
  },
173
  {
174
  "path": "ground_truth/0010.txt",
175
- "size": 194,
176
- "sha256": "2f9054687e5eb369e35d985bc2fe9d2d37ffeb8cafe204c3e81830f4431f5793"
177
  },
178
  {
179
  "path": "images/0011.png",
180
- "size": 486656,
181
- "sha256": "74e878d54295c224481f0f1b51a670459b855b16492c246c27636d71f4666e68"
182
  },
183
  {
184
  "path": "ground_truth/0011.txt",
185
- "size": 119,
186
- "sha256": "8f8bc16447a12c17e4036c4c5fd936faef227310a564de983ebe2f4f22742d83"
187
  },
188
  {
189
  "path": "images/0012.png",
190
- "size": 515472,
191
- "sha256": "9a6e49af19e893000b8e96bece22fe99fedd3741e7ac3dede04a112ea77b5b5c"
192
  },
193
  {
194
  "path": "ground_truth/0012.txt",
195
- "size": 119,
196
- "sha256": "1ccf00fe53da00ba3a2340dbe95e1692e87efccdc04b191c0f1d52dab23169a7"
197
  }
198
  ]
199
  },
200
  "thresholds": {
201
- "max_cer_delta_vs_bf16": 0.01,
202
  "max_cer_delta_vs_reference": 0.005,
203
  "max_digit_cer_delta_vs_bf16": 0.01,
204
  "max_table_score_degradation_vs_bf16": 0.01,
205
  "min_tps_ratio_vs_reference": 0.9,
206
- "max_weight_size_gb": 4.5
 
 
207
  },
208
  "metrics": {
209
  "bf16_accuracy": {
210
  "model_path": "Unlimited-OCR",
211
  "num_samples": 12,
212
- "mean_cer": 0.2391877132924964,
213
- "mean_digit_cer": 0.08733996757027829,
214
  "mean_cjk_cer": 0.3333333333333333,
215
  "mean_table_score": 1.0,
216
  "max_tokens": 1024,
@@ -219,8 +575,8 @@
219
  "reference_accuracy": {
220
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
221
  "num_samples": 12,
222
- "mean_cer": 1.057248773567081,
223
- "mean_digit_cer": 0.20915311607827292,
224
  "mean_cjk_cer": 1.0,
225
  "mean_table_score": 1.0,
226
  "max_tokens": 1024,
@@ -229,23 +585,23 @@
229
  "candidate_accuracy": {
230
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
231
  "num_samples": 12,
232
- "mean_cer": 0.23738438172232493,
233
- "mean_digit_cer": 0.08573253598528652,
234
- "mean_cjk_cer": 0.3333333333333333,
235
  "mean_table_score": 1.0,
236
  "max_tokens": 1024,
237
  "profile": "accurate"
238
  },
239
  "reference_performance": {
240
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
241
- "mean_tps": 312.7284719764298,
242
  "mean_peak_memory_mb": 5165.991419999999,
243
  "max_tokens": 256
244
  },
245
  "candidate_performance": {
246
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
247
- "mean_tps": 297.2042493840031,
248
- "mean_peak_memory_mb": 5181.5234390000005,
249
  "max_tokens": 256
250
  }
251
  },
@@ -254,23 +610,28 @@
254
  "image_path": "test_invoice.png",
255
  "prompt": "<image>document parsing.",
256
  "force_min_tokens": 8192,
 
 
 
 
 
257
  "forced_eos_token_ids": [
258
  1
259
  ],
260
  "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
261
  "processor": "arm",
262
- "baseline_memory_mb": 3844.161032,
263
  "test_results": [
264
  {
265
  "max_tokens": 512,
266
  "tokens_generated": 512,
267
  "tokens_generated_source": "mlx-vlm token count",
268
  "tokens_generated_reliable": true,
269
- "elapsed_seconds": 2.4322984160098713,
270
- "mean_tps": 264.34124967086365,
271
  "tps_source": "mlx-vlm generation_tps",
272
- "peak_memory_mb": 5181.350593,
273
- "memory_growth_mb": 1337.1895610000001,
274
  "repetition_rate": 0.0,
275
  "completed_naturally": false,
276
  "finish_reason": "length",
@@ -282,12 +643,12 @@
282
  "tokens_generated": 2048,
283
  "tokens_generated_source": "mlx-vlm token count",
284
  "tokens_generated_reliable": true,
285
- "elapsed_seconds": 8.234726583992597,
286
- "mean_tps": 261.8525825264912,
287
  "tps_source": "mlx-vlm generation_tps",
288
- "peak_memory_mb": 5181.612737,
289
- "memory_growth_mb": 1337.1895490000002,
290
- "repetition_rate": 0.09565217391304348,
291
  "completed_naturally": false,
292
  "finish_reason": "length",
293
  "forced_minimum_tokens": 2048,
@@ -298,12 +659,12 @@
298
  "tokens_generated": 8192,
299
  "tokens_generated_source": "mlx-vlm token count",
300
  "tokens_generated_reliable": true,
301
- "elapsed_seconds": 31.729143417003797,
302
- "mean_tps": 261.56365755570715,
303
  "tps_source": "mlx-vlm generation_tps",
304
- "peak_memory_mb": 5181.612737,
305
- "memory_growth_mb": 1337.1895490000002,
306
- "repetition_rate": 0.09565217391304348,
307
  "completed_naturally": false,
308
  "finish_reason": "length",
309
  "forced_minimum_tokens": 8192,
@@ -315,7 +676,7 @@
315
  "cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
316
  "tps_stable_over_length": true,
317
  "max_successful_tokens": 8192,
318
- "max_repetition_rate": 0.09565217391304348
319
  },
320
  "pass_conditions": {
321
  "cache_bounded": true,
@@ -326,14 +687,14 @@
326
  },
327
  "sensitivity": {
328
  "baseline": {
329
- "num_samples": 12,
330
- "num_digit_samples": 12,
331
  "num_table_samples": 3,
332
- "mean_cer": 0.2391877132924964,
333
- "mean_digit_cer": 0.08733996757027829,
334
  "mean_table_score": 1.0,
335
- "total_inference_time": 37.96379516503657,
336
- "avg_time_per_image": 3.163649597086381
337
  },
338
  "groups": {
339
  "vision_encoder": {
@@ -487,17 +848,17 @@
487
  "vision_model.transformer.layers.9.self_attn.qkv_proj"
488
  ],
489
  "metrics": {
490
- "num_samples": 12,
491
- "num_digit_samples": 12,
492
  "num_table_samples": 3,
493
- "mean_cer": 2.6628739674476654,
494
- "mean_digit_cer": 1.445363129900987,
495
  "mean_table_score": 0.0,
496
- "total_inference_time": 82.4734385000047,
497
- "avg_time_per_image": 6.872786541667058
498
  },
499
- "cer_delta": 2.423686254155169,
500
- "digit_cer_delta": 1.3580231623307086,
501
  "table_score_degradation": 1.0,
502
  "sensitivity_rank": 1
503
  },
@@ -508,19 +869,19 @@
508
  "projector.layers"
509
  ],
510
  "metrics": {
511
- "num_samples": 12,
512
- "num_digit_samples": 12,
513
  "num_table_samples": 3,
514
- "mean_cer": 0.23591319611894124,
515
- "mean_digit_cer": 0.08425354781719185,
516
  "mean_table_score": 1.0,
517
- "total_inference_time": 37.98282545800612,
518
- "avg_time_per_image": 3.165235454833843
519
  },
520
- "cer_delta": -0.003274517173555175,
521
- "digit_cer_delta": -0.0030864197530864335,
522
  "table_score_degradation": 0.0,
523
- "sensitivity_rank": 7
524
  },
525
  "token_embeddings": {
526
  "status": "success",
@@ -529,17 +890,17 @@
529
  "language_model.model.embed_tokens"
530
  ],
531
  "metrics": {
532
- "num_samples": 12,
533
- "num_digit_samples": 12,
534
  "num_table_samples": 3,
535
- "mean_cer": 0.2326891302977425,
536
- "mean_digit_cer": 0.08249916185227958,
537
  "mean_table_score": 1.0,
538
- "total_inference_time": 38.66650962494896,
539
- "avg_time_per_image": 3.222209135412413
540
  },
541
- "cer_delta": -0.006498582994753921,
542
- "digit_cer_delta": -0.004840805717998706,
543
  "table_score_degradation": 0.0,
544
  "sensitivity_rank": 10
545
  },
@@ -561,17 +922,17 @@
561
  "language_model.model.layers.9.self_attn.q_proj"
562
  ],
563
  "metrics": {
564
- "num_samples": 12,
565
- "num_digit_samples": 12,
566
  "num_table_samples": 3,
567
- "mean_cer": 0.21507560490000377,
568
- "mean_digit_cer": 0.09653424957157782,
569
  "mean_table_score": 1.0,
570
- "total_inference_time": 38.38538125100604,
571
- "avg_time_per_image": 3.19878177091717
572
  },
573
- "cer_delta": -0.024112108392492643,
574
- "digit_cer_delta": 0.009194282001299528,
575
  "table_score_degradation": 0.0,
576
  "sensitivity_rank": 11
577
  },
@@ -593,17 +954,17 @@
593
  "language_model.model.layers.9.self_attn.k_proj"
594
  ],
595
  "metrics": {
596
- "num_samples": 12,
597
- "num_digit_samples": 12,
598
  "num_table_samples": 3,
599
- "mean_cer": 0.23424214947533464,
600
- "mean_digit_cer": 0.08425354781719185,
601
  "mean_table_score": 1.0,
602
- "total_inference_time": 38.175402873021085,
603
- "avg_time_per_image": 3.181283572751757
604
  },
605
- "cer_delta": -0.004945563817161769,
606
- "digit_cer_delta": -0.0030864197530864335,
607
  "table_score_degradation": 0.0,
608
  "sensitivity_rank": 9
609
  },
@@ -625,19 +986,19 @@
625
  "language_model.model.layers.9.self_attn.v_proj"
626
  ],
627
  "metrics": {
628
- "num_samples": 12,
629
- "num_digit_samples": 12,
630
  "num_table_samples": 3,
631
- "mean_cer": 0.23642534441989824,
632
- "mean_digit_cer": 0.08425354781719185,
633
  "mean_table_score": 1.0,
634
- "total_inference_time": 41.72621229101787,
635
- "avg_time_per_image": 3.477184357584823
636
  },
637
- "cer_delta": -0.0027623688725981754,
638
- "digit_cer_delta": -0.0030864197530864335,
639
  "table_score_degradation": 0.0,
640
- "sensitivity_rank": 5
641
  },
642
  "attention_o_proj": {
643
  "status": "success",
@@ -657,19 +1018,19 @@
657
  "language_model.model.layers.9.self_attn.o_proj"
658
  ],
659
  "metrics": {
660
- "num_samples": 12,
661
- "num_digit_samples": 12,
662
  "num_table_samples": 3,
663
- "mean_cer": 0.2359826017166606,
664
- "mean_digit_cer": 0.07714420469193685,
665
  "mean_table_score": 1.0,
666
- "total_inference_time": 41.9442965429771,
667
- "avg_time_per_image": 3.4953580452480915
668
  },
669
- "cer_delta": -0.003205111575835806,
670
- "digit_cer_delta": -0.010195762878341438,
671
  "table_score_degradation": 0.0,
672
- "sensitivity_rank": 6
673
  },
674
  "dense_mlp": {
675
  "status": "success",
@@ -680,19 +1041,19 @@
680
  "language_model.model.layers.0.mlp.up_proj"
681
  ],
682
  "metrics": {
683
- "num_samples": 12,
684
- "num_digit_samples": 12,
685
  "num_table_samples": 3,
686
- "mean_cer": 0.23568756434998817,
687
- "mean_digit_cer": 0.08425354781719185,
688
  "mean_table_score": 1.0,
689
- "total_inference_time": 41.424109207015135,
690
- "avg_time_per_image": 3.4520091005845948
691
  },
692
- "cer_delta": -0.0035001489425082466,
693
- "digit_cer_delta": -0.0030864197530864335,
694
  "table_score_degradation": 0.0,
695
- "sensitivity_rank": 8
696
  },
697
  "shared_experts": {
698
  "status": "success",
@@ -733,19 +1094,19 @@
733
  "language_model.model.layers.9.mlp.shared_experts.up_proj"
734
  ],
735
  "metrics": {
736
- "num_samples": 12,
737
- "num_digit_samples": 12,
738
  "num_table_samples": 3,
739
- "mean_cer": 0.23649792801080774,
740
- "mean_digit_cer": 0.09592829886362793,
741
  "mean_table_score": 1.0,
742
- "total_inference_time": 37.84185174998129,
743
- "avg_time_per_image": 3.153487645831774
744
  },
745
- "cer_delta": -0.0026897852816886725,
746
- "digit_cer_delta": 0.00858833129334964,
747
  "table_score_degradation": 0.0,
748
- "sensitivity_rank": 4
749
  },
750
  "routed_experts": {
751
  "status": "success",
@@ -786,19 +1147,19 @@
786
  "language_model.model.layers.9.mlp.switch_mlp.up_proj"
787
  ],
788
  "metrics": {
789
- "num_samples": 12,
790
- "num_digit_samples": 12,
791
  "num_table_samples": 3,
792
- "mean_cer": 0.23911822127011442,
793
- "mean_digit_cer": 0.08452479009169811,
794
  "mean_table_score": 1.0,
795
- "total_inference_time": 36.668351295011234,
796
- "avg_time_per_image": 3.0556959412509364
797
  },
798
- "cer_delta": -6.949202238198948e-05,
799
- "digit_cer_delta": -0.0028151774785801775,
800
  "table_score_degradation": 0.0,
801
- "sensitivity_rank": 3
802
  },
803
  "lm_head": {
804
  "status": "success",
@@ -807,57 +1168,59 @@
807
  "language_model.lm_head"
808
  ],
809
  "metrics": {
810
- "num_samples": 12,
811
- "num_digit_samples": 12,
812
  "num_table_samples": 3,
813
- "mean_cer": 0.25282999056583605,
814
- "mean_digit_cer": 0.12301897991595728,
815
  "mean_table_score": 1.0,
816
- "total_inference_time": 37.152046875009546,
817
- "avg_time_per_image": 3.0960039062507954
818
  },
819
- "cer_delta": 0.013642277273339637,
820
- "digit_cer_delta": 0.035679012345678995,
821
  "table_score_degradation": 0.0,
822
- "sensitivity_rank": 2
823
  }
824
  }
825
  },
826
  "calibration": {
827
- "schema_version": 1,
828
- "created_at": "2026-07-20T23:25:55.306169+00:00",
829
  "target_pattern": "language_model.lm_head",
830
  "selection_policy": "fastest candidate passing existing quality and throughput limits",
831
  "thresholds": {
832
- "max_cer_delta_vs_bf16": 0.01,
833
  "max_cer_delta_vs_reference": 0.005,
834
  "max_digit_cer_delta_vs_bf16": 0.01,
835
  "max_table_score_degradation_vs_bf16": 0.01,
836
  "min_tps_ratio_vs_reference": 0.9,
837
- "max_weight_size_gb": 4.5
 
 
838
  },
839
  "experiments": [
840
  {
841
  "label": "bf16-head",
842
  "precision": "bfloat16",
843
- "passed": false,
844
  "checks": {
845
  "cer": true,
846
  "digit_cer": true,
847
  "table_score": true,
848
- "throughput": false
849
  },
850
  "metrics": {
851
- "mean_cer": 0.23190356458231498,
852
- "mean_digit_cer": 0.08222376405546196,
853
  "mean_table_score": 1.0,
854
- "mean_tps": 271.0950821181683
855
  },
856
  "deltas": {
857
- "cer_vs_bf16": -0.007284148710181432,
858
- "digit_cer_vs_bf16": -0.005116203514816328,
859
  "table_degradation_vs_bf16": 0.0,
860
- "tps_ratio_vs_reference": 0.8668704848166195
861
  }
862
  },
863
  {
@@ -865,22 +1228,22 @@
865
  "precision": "mxfp8",
866
  "passed": false,
867
  "checks": {
868
- "cer": false,
869
- "digit_cer": true,
870
  "table_score": true,
871
  "throughput": true
872
  },
873
  "metrics": {
874
- "mean_cer": 0.26017003915600867,
875
- "mean_digit_cer": 0.0769606061607251,
876
  "mean_table_score": 1.0,
877
- "mean_tps": 307.3938836867251
878
  },
879
  "deltas": {
880
- "cer_vs_bf16": 0.020982325863512258,
881
- "digit_cer_vs_bf16": -0.010379361409553187,
882
  "table_degradation_vs_bf16": 0.0,
883
- "tps_ratio_vs_reference": 0.9829417889071937
884
  }
885
  },
886
  {
@@ -894,16 +1257,16 @@
894
  "throughput": true
895
  },
896
  "metrics": {
897
- "mean_cer": 0.23738438172232493,
898
- "mean_digit_cer": 0.08573253598528652,
899
  "mean_table_score": 1.0,
900
- "mean_tps": 301.24796539578256
901
  },
902
  "deltas": {
903
- "cer_vs_bf16": -0.0018033315701714847,
904
- "digit_cer_vs_bf16": -0.0016074315849917697,
905
  "table_degradation_vs_bf16": 0.0,
906
- "tps_ratio_vs_reference": 0.9632892185732532
907
  }
908
  }
909
  ],
@@ -913,11 +1276,170 @@
913
  },
914
  "precision_overrides": {
915
  "language_model.lm_head": "affine8"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
916
  }
917
  },
918
  "precision_map": {
919
  "vision_model": "bfloat16",
920
- "projector": "mxfp8",
921
  "language_model.model.embed_tokens": "mxfp8",
922
  "language_model.model.norm": "bfloat16",
923
  "language_model.lm_head": "affine8",
@@ -949,16 +1471,16 @@
949
  "vision_encoder": {
950
  "precision": "bfloat16",
951
  "reasons": [
952
- "cer_delta=2.423686",
953
- "digit_cer_delta=1.358023",
954
  "table_score_degradation=1.000000",
955
  "protected OCR-critical group"
956
  ]
957
  },
958
  "vision_projector": {
959
- "precision": "mxfp8",
960
  "reasons": [
961
- "all measured quality deltas were non-degrading"
962
  ]
963
  },
964
  "token_embeddings": {
@@ -1026,69 +1548,109 @@
1026
  }
1027
  },
1028
  "artifacts": {
1029
- "bf16_accuracy": {
1030
- "filename": "bf16_accuracy.json",
1031
- "size": 4857,
1032
- "sha256": "0de8511f1ba98f362d3ab712522115e534d7f129a8b587751ba5d5d8c95f272d"
1033
- },
1034
  "reference_accuracy": {
1035
  "filename": "reference_accuracy.json",
1036
- "size": 4923,
1037
- "sha256": "354d3c17451bc9e1c6ec645d36ba2e133cd1b2393dae2d7825628832e11dc365"
1038
  },
1039
- "candidate_accuracy": {
1040
- "filename": "candidate_accuracy.json",
1041
- "size": 4863,
1042
- "sha256": "20dbd578c93afa885a2b98627b5e8335b3c68ef14e84afa66bc9392f7e6c5daf"
1043
- },
1044
- "reference_performance": {
1045
- "filename": "reference_performance.json",
1046
- "size": 2211,
1047
- "sha256": "74a5983677fcedab1cfe8193cc4138abfb011c6e9f2fa90d954cf3b52086888a"
1048
  },
1049
  "candidate_performance": {
1050
  "filename": "candidate_performance.json",
1051
- "size": 2219,
1052
- "sha256": "52a7760cfcc1ce1f588fbce0642951874800fcb7952bed53644d81d91170862c"
1053
- },
1054
- "candidate_rswa": {
1055
- "filename": "candidate_rswa.json",
1056
- "size": 2374,
1057
- "sha256": "612e0250c99eae03d4746f3687af57ab4ee43500fab376cd59d69be460ffa429"
1058
  },
1059
  "sensitivity_results": {
1060
  "filename": "sensitivity_results.json",
1061
- "size": 22710,
1062
- "sha256": "63435bddcfe5d8e96921ed53eb1741ca731c9d362ab549391a436342b147d415"
1063
  },
1064
  "calibration_results": {
1065
  "filename": "calibration_results.json",
1066
- "size": 2542,
1067
- "sha256": "416c1f8e332d69d9a753bea140e264a5573875f283c9eadc79b3ea61addc3d7b"
1068
  },
1069
- "generated_precision_map": {
1070
- "filename": "generated_precision_map.json",
1071
- "size": 3315,
1072
- "sha256": "df9d9f6b2490d75e8589350b481fb1916a6dbf8a141c5f6958b0525d72fb1a46"
 
 
 
 
 
 
 
 
 
 
1073
  },
1074
  "provenance": {
1075
  "filename": "provenance.json",
1076
- "size": 5389,
1077
- "sha256": "03c82c24826fdd6fdc0f5031ba67394979753584ff8949482a739dfef1719b47"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
  }
1079
  },
1080
  "gates": [
1081
  {
1082
  "name": "weights_are_distinct",
1083
  "passed": true,
1084
- "actual": "a4f6e9934f90564906f42929542ad68493f0c6187ad431128e447debbf52b6f4",
1085
  "limit": "different from 439c6acfe5e277537dfe3368b94a145a6b0da4c39e4bd00582977d030f292ad1",
1086
  "detail": "Candidate aggregate digest must differ from the Sahil reference"
1087
  },
1088
  {
1089
  "name": "weight_size_gb",
1090
  "passed": true,
1091
- "actual": 3.5792221864685416,
1092
  "limit": 4.5,
1093
  "detail": "Candidate Safetensors size"
1094
  },
@@ -1101,11 +1663,30 @@
1101
  "mxfp8": true,
1102
  "sliding_window": true,
1103
  "processor_class": true,
1104
- "sft_format": true
 
 
 
1105
  },
1106
  "limit": true,
1107
  "detail": "Native Unlimited-OCR, MXFP8, and R-SWA metadata"
1108
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1109
  {
1110
  "name": "evaluation_coverage",
1111
  "passed": true,
@@ -1119,34 +1700,168 @@
1119
  "detail": "Dataset must cover ordinary text, digits, CJK, and tables"
1120
  },
1121
  {
1122
- "name": "same_evaluation_samples",
 
 
 
 
 
 
 
 
 
 
 
 
 
1123
  "passed": true,
1124
  "actual": [
1125
- 12,
1126
- 12,
1127
- 12
 
 
 
 
 
1128
  ],
1129
- "limit": 12,
1130
- "detail": "All three checkpoints must run the complete evaluation set"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1131
  },
1132
  {
1133
  "name": "candidate_cer_vs_bf16",
1134
  "passed": true,
1135
- "actual": -0.0018033315701714847,
1136
- "limit": 0.01,
1137
  "detail": "Candidate minus BF16 absolute mean CER"
1138
  },
1139
  {
1140
  "name": "candidate_cer_vs_reference",
1141
  "passed": true,
1142
- "actual": -0.8198643918447561,
1143
  "limit": 0.005,
1144
  "detail": "Candidate minus Sahil-reference absolute mean CER"
1145
  },
1146
  {
1147
  "name": "candidate_digit_cer_vs_bf16",
1148
  "passed": true,
1149
- "actual": -0.0016074315849917697,
1150
  "limit": 0.01,
1151
  "detail": "Candidate minus BF16 digit CER"
1152
  },
@@ -1157,10 +1872,73 @@
1157
  "limit": 0.01,
1158
  "detail": "BF16 minus candidate mean table score"
1159
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1160
  {
1161
  "name": "candidate_tps_vs_reference",
1162
  "passed": true,
1163
- "actual": 0.9503587809120344,
1164
  "limit": 0.9,
1165
  "detail": "Candidate decode throughput divided by Sahil-reference throughput"
1166
  },
@@ -1168,12 +1946,134 @@
1168
  "name": "rswa_8k_bounded",
1169
  "passed": true,
1170
  "actual": {
1171
- "cache_bounded": true,
1172
- "tps_stable": true,
1173
- "8k_test_passed": true
 
 
 
 
1174
  },
1175
- "limit": true,
1176
- "detail": "8K generation, bounded cache, and stable throughput"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1177
  }
1178
  ],
1179
  "environment": {
 
1
  {
2
+ "schema_version": 3,
3
+ "created_at": "2026-07-21T05:26:20.621136+00:00",
4
  "release_approved": true,
5
  "repo_id": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
6
  "source": {
7
  "id": "baidu/Unlimited-OCR",
8
+ "resolved_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
9
  "weights": {
10
  "files": [
11
  {
 
21
  },
22
  "reference": {
23
  "id": "sahilchachra/unlimited-ocr-mxfp8-mlx",
24
+ "resolved_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
25
  "weights": {
26
  "files": [
27
  {
 
41
  "files": [
42
  {
43
  "name": "model.safetensors",
44
+ "size": 3845699993,
45
+ "sha256": "d9fe11fc8d2e333000be6a0e288681231efa69282c844f7775ca40f52db02f0c"
46
  }
47
  ],
48
+ "total_size_bytes": 3845699993,
49
+ "total_size_gb": 3.5815872186794877,
50
+ "aggregate_sha256": "5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549"
51
+ },
52
+ "files": {
53
+ "files": [
54
+ {
55
+ "path": "config.json",
56
+ "size": 33815,
57
+ "sha256": "2c3eeb61157f6b9a5c42aa0771771911516e3ac8b3cbd9b61d1532a33e84a2bd"
58
+ },
59
+ {
60
+ "path": "model.safetensors.index.json",
61
+ "size": 61701,
62
+ "sha256": "bb6693a47e22e596139e90d8a1442a64155900d27aa0e2419ec071ef4f0ad4a3"
63
+ },
64
+ {
65
+ "path": "processor_config.json",
66
+ "size": 454,
67
+ "sha256": "e9f224a7f0d2263bf62efc052e93681720e31f9e34b1c42134d2371e8708a426"
68
+ },
69
+ {
70
+ "path": "special_tokens_map.json",
71
+ "size": 801,
72
+ "sha256": "ab4bd57ce17d62e39e0a39e739de1e407484f090f0b2c7e391312bca7a5b061a"
73
+ },
74
+ {
75
+ "path": "tokenizer.json",
76
+ "size": 9979544,
77
+ "sha256": "a02f8fd5228c90256bb4f6554c34a579d48f909e5beb232dc4afad870b55a8b4"
78
+ },
79
+ {
80
+ "path": "tokenizer_config.json",
81
+ "size": 569,
82
+ "sha256": "86f9fcd3f04060ba47a616241f5fc3f65b53a54ee7260367089467cdca849568"
83
+ },
84
+ {
85
+ "path": "chat_template.jinja",
86
+ "size": 191,
87
+ "sha256": "b4e4771e69892ebc712ed6986bcce490c65041d1545e85599cd4a531cfab98a5"
88
+ },
89
+ {
90
+ "path": "precision_map.json",
91
+ "size": 3337,
92
+ "sha256": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75"
93
+ },
94
+ {
95
+ "path": "quantization_summary.json",
96
+ "size": 15572,
97
+ "sha256": "417dbf5b59f6064361f6096cbf59bbe1774e7562301a49f47100fe4ceb201c55"
98
+ }
99
+ ],
100
+ "aggregate_sha256": "cbd09caa91f51dd7abf2ebb16eb38d20a71ff00e21056a985f8bfd28269ac97c"
101
  },
102
  "metadata": {
103
  "passed": true,
 
107
  "mxfp8": true,
108
  "sliding_window": true,
109
  "processor_class": true,
110
+ "sft_format": true,
111
+ "precision_map": true,
112
+ "quantization_summary": true,
113
+ "protected_modules_preserved": true
114
  },
115
+ "sliding_window": 128,
116
+ "source_model": "Unlimited-OCR",
117
+ "source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead"
118
  }
119
  },
120
+ "project_files": {
121
+ "files": [
122
+ {
123
+ "path": "LICENSE",
124
+ "size": 1067,
125
+ "sha256": "6268bc8a51423daeb65e5b0225d9a73fd87b464e235bebc02ceaccc1d1779f45"
126
+ },
127
+ {
128
+ "path": "pyproject.toml",
129
+ "size": 2060,
130
+ "sha256": "7441c069b03a90e26b1997e94d52edb2d8a7ce0d5c012093080d143e79d19ba2"
131
+ },
132
+ {
133
+ "path": "generation_config.json",
134
+ "size": 802,
135
+ "sha256": "69ac71fd1613d81cb83d07fb328ad6bf24eab5584fd5348665dad2497782954d"
136
+ },
137
+ {
138
+ "path": "preprocessing_config.json",
139
+ "size": 3033,
140
+ "sha256": "a269fce81f50bdabec9f7fac0219ccd9de62e2b7fd9d8a31f313b04e52b70432"
141
+ },
142
+ {
143
+ "path": "quantization_config.json",
144
+ "size": 1441,
145
+ "sha256": "cc5d461002cece610848d697aa293a41b57edcebd93020348451dbe4de1d2481"
146
+ },
147
+ {
148
+ "path": "docs/PRD.md",
149
+ "size": 6915,
150
+ "sha256": "c64527daf7d9b591a453be856d8a8733f8dd23f8a8e4c913cf47b49ff17786c1"
151
+ },
152
+ {
153
+ "path": "docs/TECHNICAL_SPEC.md",
154
+ "size": 10379,
155
+ "sha256": "3967f46ee59728e75563907ec8f0e93d08a5da320d28395c4e3084a1ff68a3db"
156
+ },
157
+ {
158
+ "path": "docs/adr/0001-verified-ocr-aware-quantization.md",
159
+ "size": 3312,
160
+ "sha256": "e5c4996d80263bcaf43a60cd91db45c214873486eee858fe0ac3f4762fde5e21"
161
+ },
162
+ {
163
+ "path": "docs/adr/0002-calibrated-affine8-lm-head.md",
164
+ "size": 2722,
165
+ "sha256": "4606afa17413791e4bd4acc2e959162fc98e6978a67b236d201add396e31d5ea"
166
+ },
167
+ {
168
+ "path": "src/unlimited_ocr/__init__.py",
169
+ "size": 533,
170
+ "sha256": "5f46da23ba70ec43fadd5e3b4b5f9335063e4fc86e2ef20d23e33f8722fb8d19"
171
+ },
172
+ {
173
+ "path": "src/unlimited_ocr/engine.py",
174
+ "size": 24024,
175
+ "sha256": "b66e97254d317bf4b8886daa3d01aecae71237590d6bb249af08fd6c9831da1c"
176
+ },
177
+ {
178
+ "path": "src/unlimited_ocr/pipeline.py",
179
+ "size": 26762,
180
+ "sha256": "baf259a4e46282b031cbdc71d1538dc68c2293935ff3581233d3d0b41e32d666"
181
+ },
182
+ {
183
+ "path": "src/unlimited_ocr/preprocessing.py",
184
+ "size": 28532,
185
+ "sha256": "d553b667a1c83ad1aedf63fdf642227a829dab66c705562e1c9d718f3ffe898d"
186
+ },
187
+ {
188
+ "path": "src/unlimited_ocr/output.py",
189
+ "size": 15061,
190
+ "sha256": "c0f42a4c454ad39ef62714575d8f0c391ba5572edb9a79b9d06c7049382964df"
191
+ },
192
+ {
193
+ "path": "src/unlimited_ocr/pdf.py",
194
+ "size": 8659,
195
+ "sha256": "3c23ea843bd06aa0712aabdf7c164270c8d363e32271a4e13a446ffdec05a460"
196
+ },
197
+ {
198
+ "path": "src/unlimited_ocr/cli.py",
199
+ "size": 20469,
200
+ "sha256": "c6b813ef2a01a960369bbff79d270cb335f41b92ad3435b24cf2665cc2d2c6f5"
201
+ },
202
+ {
203
+ "path": "src/unlimited_ocr/profiles.py",
204
+ "size": 2917,
205
+ "sha256": "ce31200aeb9c8e459b258e3258c0ded76ca40de0d78d07b8cb6bdd05ed45653f"
206
+ },
207
+ {
208
+ "path": "src/unlimited_ocr/server.py",
209
+ "size": 12153,
210
+ "sha256": "7723c3931e610815efc64477f3e7bd41ee851d9aee53798f42842e13ffdce2d1"
211
+ },
212
+ {
213
+ "path": "src/unlimited_ocr/adapter_registry.json",
214
+ "size": 880,
215
+ "sha256": "b06c2778707788f75a5f5c12097246c06126a6adca4bfbd8e4ba40fc6690ad6b"
216
+ },
217
+ {
218
+ "path": "quantization/layer_sensitivity.py",
219
+ "size": 14664,
220
+ "sha256": "06b954efcfa58eb00a02e3db77b46857a13c5fcab5b1422350cda062c577f7f3"
221
+ },
222
+ {
223
+ "path": "quantization/calibrate_precision.py",
224
+ "size": 10666,
225
+ "sha256": "0edaa2d14f435288fff96e876ea5eab8111232b34a802708bf354ee7a259c3c2"
226
+ },
227
+ {
228
+ "path": "quantization/mixed_precision_convert.py",
229
+ "size": 13954,
230
+ "sha256": "33b01c48b0f945871633c91e46e063549062730ae7792d2540a8a0a59dd193fe"
231
+ },
232
+ {
233
+ "path": "quantization/precision_map.json",
234
+ "size": 1743,
235
+ "sha256": "d7965210b5cab6afbde056cc91a33a41b651a254a9fa8db0b14dcdf85f7cbcbf"
236
+ },
237
+ {
238
+ "path": "quantization/release_gate.py",
239
+ "size": 83309,
240
+ "sha256": "87e9e2c4f3e94345009f698ec6222a4b78172693ac4c9444571cf34e100d5498"
241
+ },
242
+ {
243
+ "path": "quantization/run_pipeline.py",
244
+ "size": 23650,
245
+ "sha256": "35229b866f7433397d479e2795248d21b9d02b9d7e7000d01440ffd23c9162ec"
246
+ },
247
+ {
248
+ "path": "benchmarks/evaluate_cer.py",
249
+ "size": 9407,
250
+ "sha256": "e6c5010a5886e3e5342825f2b970d792bf02e4ef79e44284de74702f2745e423"
251
+ },
252
+ {
253
+ "path": "benchmarks/evaluate_tables.py",
254
+ "size": 9554,
255
+ "sha256": "a5ff42da5083eff0b70d7dc6e351395338198c1b860a3d6565e2352b159850a2"
256
+ },
257
+ {
258
+ "path": "benchmarks/normalize_output.py",
259
+ "size": 4059,
260
+ "sha256": "dfceeb07c0df48f6f4ed925f990a45b5f8f69fc0dfc37f0fd377cea6073196e4"
261
+ },
262
+ {
263
+ "path": "benchmarks/run_accuracy.py",
264
+ "size": 16808,
265
+ "sha256": "9e937e32b5c3599c07733f1046184624b2a0c40b68e307058c3fd141c383994e"
266
+ },
267
+ {
268
+ "path": "benchmarks/run_performance.py",
269
+ "size": 12345,
270
+ "sha256": "8f87f459b1f4778afdfc20c078154d954edf521699b273f610132d0371d1c57a"
271
+ },
272
+ {
273
+ "path": "benchmarks/rswa_validation.py",
274
+ "size": 21969,
275
+ "sha256": "0de31e66650604cfbdd5a48b85470002bbe447bca7caa7a0aa7ca48ac8512215"
276
+ },
277
+ {
278
+ "path": "benchmarks/datasets.md",
279
+ "size": 3185,
280
+ "sha256": "47902cdf95278dc9509057fe64018251dce43e775fa7582590d39a916483d983"
281
+ },
282
+ {
283
+ "path": "examples/single_image.py",
284
+ "size": 1123,
285
+ "sha256": "87afe046d745fe62e78eb584b35e8dffcd9579c13cf2f01b483b9d4ea9427cc5"
286
+ },
287
+ {
288
+ "path": "examples/multi_page_pdf.py",
289
+ "size": 1182,
290
+ "sha256": "cc335ed4d4e55f1b0818813d6fbd00319a1fb4675fda8b6c5803b5fdf137ae37"
291
+ },
292
+ {
293
+ "path": "examples/batch_directory.py",
294
+ "size": 1411,
295
+ "sha256": "d9bc32620e48fc5bcbb0504b3a16a90de9a35d1083d56584b2fce1e2591a928d"
296
+ }
297
+ ],
298
+ "aggregate_sha256": "68de2e9d2c503b3b2023d0b55ff0f7446d2046ac5a3285b5e9e23b8baeb6a370"
299
+ },
300
+ "calibration_dataset": {
301
+ "name": "calibration",
302
+ "num_samples": 10,
303
+ "num_digit_samples": 10,
304
  "num_cjk_samples": 3,
305
  "num_table_samples": 3,
306
  "category_counts": {
307
  "cjk_document": 3,
308
+ "english_document": 2,
309
  "low_quality_scan": 2,
310
  "table_numeric": 3
311
  },
312
+ "manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
313
+ "content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
314
  "files": [
315
  {
316
  "path": "images/0001.png",
 
334
  },
335
  {
336
  "path": "images/0003.png",
337
+ "size": 40039,
338
+ "sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
339
+ },
340
+ {
341
+ "path": "ground_truth/0003.txt",
342
+ "size": 459,
343
+ "sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
344
+ },
345
+ {
346
+ "path": "images/0004.png",
347
+ "size": 40356,
348
+ "sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
349
+ },
350
+ {
351
+ "path": "ground_truth/0004.txt",
352
+ "size": 459,
353
+ "sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
354
+ },
355
+ {
356
+ "path": "images/0005.png",
357
+ "size": 40650,
358
+ "sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
359
+ },
360
+ {
361
+ "path": "ground_truth/0005.txt",
362
+ "size": 459,
363
+ "sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
364
+ },
365
+ {
366
+ "path": "images/0006.png",
367
+ "size": 46036,
368
+ "sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
369
+ },
370
+ {
371
+ "path": "ground_truth/0006.txt",
372
+ "size": 191,
373
+ "sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
374
+ },
375
+ {
376
+ "path": "images/0007.png",
377
+ "size": 41828,
378
+ "sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
379
+ },
380
+ {
381
+ "path": "ground_truth/0007.txt",
382
+ "size": 195,
383
+ "sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
384
+ },
385
+ {
386
+ "path": "images/0008.png",
387
+ "size": 20975,
388
+ "sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
389
+ },
390
+ {
391
+ "path": "ground_truth/0008.txt",
392
+ "size": 201,
393
+ "sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
394
+ },
395
+ {
396
+ "path": "images/0009.png",
397
+ "size": 487313,
398
+ "sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
399
+ },
400
+ {
401
+ "path": "ground_truth/0009.txt",
402
+ "size": 121,
403
+ "sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
404
+ },
405
+ {
406
+ "path": "images/0010.png",
407
+ "size": 515820,
408
+ "sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
409
+ },
410
+ {
411
+ "path": "ground_truth/0010.txt",
412
+ "size": 121,
413
+ "sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
414
+ }
415
+ ]
416
+ },
417
+ "evaluation_dataset": {
418
+ "name": "evaluation",
419
+ "num_samples": 12,
420
+ "num_digit_samples": 12,
421
+ "num_cjk_samples": 3,
422
+ "num_table_samples": 3,
423
+ "category_counts": {
424
+ "cjk_document": 3,
425
+ "english_document": 4,
426
+ "low_quality_scan": 2,
427
+ "table_numeric": 3
428
+ },
429
+ "manifest_sha256": "111646a6f59ed32689fbc658c1cb4ed60629e1bbd909d6137df469e7b489f121",
430
+ "content_sha256": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
431
+ "files": [
432
+ {
433
+ "path": "images/0001.png",
434
  "size": 181040,
435
  "sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
436
  },
437
  {
438
+ "path": "ground_truth/0001.txt",
439
  "size": 1333,
440
  "sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
441
  },
442
  {
443
+ "path": "images/0002.png",
444
  "size": 220208,
445
  "sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
446
  },
447
  {
448
+ "path": "ground_truth/0002.txt",
449
  "size": 1330,
450
  "sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
451
  },
452
+ {
453
+ "path": "images/0003.png",
454
+ "size": 119613,
455
+ "sha256": "bcf22c94459b970f1ee11088d8a902dbc56975e487c489b4714dedf8f3a4dca1"
456
+ },
457
+ {
458
+ "path": "ground_truth/0003.txt",
459
+ "size": 637,
460
+ "sha256": "a035ed288c8cc0e73e6cddc08c804be9b1ed32eee98e7f85c0e027bd88221b66"
461
+ },
462
+ {
463
+ "path": "images/0004.png",
464
+ "size": 67701,
465
+ "sha256": "baea8365beb81cc9939e59d3c8a0b88ab17999122eb1729e33214450a4fba987"
466
+ },
467
+ {
468
+ "path": "ground_truth/0004.txt",
469
+ "size": 478,
470
+ "sha256": "af1c29967e55595362ab41e25000e00167474cbe38ea43b8816f1f1395ef6e72"
471
+ },
472
  {
473
  "path": "images/0005.png",
474
+ "size": 41300,
475
+ "sha256": "65662634fc3b5df7d006f40d276fb2869809a86a2027f464c1d6776094e16f16"
476
  },
477
  {
478
  "path": "ground_truth/0005.txt",
479
+ "size": 459,
480
+ "sha256": "1d565459641aca5056fdcccabe2cdc6834107bef219645a54cd28db2e06b05ea"
481
  },
482
  {
483
  "path": "images/0006.png",
484
+ "size": 40574,
485
+ "sha256": "67dbd4d07cb6e3f971937dc93a35ac81775580ee4d8895373afe6ae7e6b328dd"
486
  },
487
  {
488
  "path": "ground_truth/0006.txt",
489
+ "size": 459,
490
+ "sha256": "0e134815b0bee585e3841d5a22a296c136fb43f00a9e3d48d98d2df24e884b08"
491
  },
492
  {
493
  "path": "images/0007.png",
494
+ "size": 41027,
495
+ "sha256": "2b54b5222174bf4d41d92a6ee97aa1b2a3e57daca987b55906793df807dfe08f"
496
  },
497
  {
498
  "path": "ground_truth/0007.txt",
499
+ "size": 459,
500
+ "sha256": "b63a7c471f4d36cc8bafddcbd7b4a8d92d6f92e73ab1e260505c2413d6e39146"
501
  },
502
  {
503
  "path": "images/0008.png",
504
+ "size": 46025,
505
+ "sha256": "c8c8ed310cbdebf7e27adf3361e6ce5939922bd674af12205cea441a9b53399d"
506
  },
507
  {
508
  "path": "ground_truth/0008.txt",
509
+ "size": 191,
510
+ "sha256": "246e79c3ef9faa52dd10c400eecf594314493c88ae5f7794a6129dd90e850107"
511
  },
512
  {
513
  "path": "images/0009.png",
514
+ "size": 41617,
515
+ "sha256": "a00eaebf40f9369b6851a949a0aedc82a57cf1104a957e42880b1f3aa65606f0"
516
  },
517
  {
518
  "path": "ground_truth/0009.txt",
519
+ "size": 195,
520
+ "sha256": "5653ef6df8a330496e73c6ddd71c9b2280818e13b11b6ff8842622ad53d24a5b"
521
  },
522
  {
523
  "path": "images/0010.png",
524
+ "size": 21089,
525
+ "sha256": "59b6edec6b2ef0369f1d7fbe9fe96648fda2982db0eae377199d5edf26855900"
526
  },
527
  {
528
  "path": "ground_truth/0010.txt",
529
+ "size": 201,
530
+ "sha256": "6d2f6474c94dea173288aebde046b6db40534548e8187cb776dc494abaa04ce2"
531
  },
532
  {
533
  "path": "images/0011.png",
534
+ "size": 487332,
535
+ "sha256": "70f57ebf6f41479e443e4356422e6dd619ffdcf2f0df7837943f5427545cd266"
536
  },
537
  {
538
  "path": "ground_truth/0011.txt",
539
+ "size": 121,
540
+ "sha256": "f550334b0b7d3508ef898d0e144593c6d005dd32d1bbcb837aeb650da56e9eb8"
541
  },
542
  {
543
  "path": "images/0012.png",
544
+ "size": 516088,
545
+ "sha256": "fdd426f69928ba7bcc6008655fa0dbd950b6844eba8322491e7a5399bf052d59"
546
  },
547
  {
548
  "path": "ground_truth/0012.txt",
549
+ "size": 121,
550
+ "sha256": "d040adc39553bc337df562486b3b5148727660214ec79a3af48b9cf5a2bc23f7"
551
  }
552
  ]
553
  },
554
  "thresholds": {
555
+ "max_cer_delta_vs_bf16": 0.015,
556
  "max_cer_delta_vs_reference": 0.005,
557
  "max_digit_cer_delta_vs_bf16": 0.01,
558
  "max_table_score_degradation_vs_bf16": 0.01,
559
  "min_tps_ratio_vs_reference": 0.9,
560
+ "max_weight_size_gb": 4.5,
561
+ "min_rswa_tokens": 8192,
562
+ "max_rswa_repetition_rate": 0.25
563
  },
564
  "metrics": {
565
  "bf16_accuracy": {
566
  "model_path": "Unlimited-OCR",
567
  "num_samples": 12,
568
+ "mean_cer": 0.205212265458972,
569
+ "mean_digit_cer": 0.11330969496189873,
570
  "mean_cjk_cer": 0.3333333333333333,
571
  "mean_table_score": 1.0,
572
  "max_tokens": 1024,
 
575
  "reference_accuracy": {
576
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
577
  "num_samples": 12,
578
+ "mean_cer": 0.9073812810686629,
579
+ "mean_digit_cer": 0.4065110611974061,
580
  "mean_cjk_cer": 1.0,
581
  "mean_table_score": 1.0,
582
  "max_tokens": 1024,
 
585
  "candidate_accuracy": {
586
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
587
  "num_samples": 12,
588
+ "mean_cer": 0.21633091868789586,
589
+ "mean_digit_cer": 0.11193906322597019,
590
+ "mean_cjk_cer": 0.3412698412698412,
591
  "mean_table_score": 1.0,
592
  "max_tokens": 1024,
593
  "profile": "accurate"
594
  },
595
  "reference_performance": {
596
  "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
597
+ "mean_tps": 35.07146198590831,
598
  "mean_peak_memory_mb": 5165.991419999999,
599
  "max_tokens": 256
600
  },
601
  "candidate_performance": {
602
  "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
603
+ "mean_tps": 35.14004923388335,
604
+ "mean_peak_memory_mb": 5184.062959,
605
  "max_tokens": 256
606
  }
607
  },
 
610
  "image_path": "test_invoice.png",
611
  "prompt": "<image>document parsing.",
612
  "force_min_tokens": 8192,
613
+ "generation_settings": {
614
+ "temperature": 0.0,
615
+ "no_repeat_ngram_size": 35,
616
+ "ngram_window": 128
617
+ },
618
  "forced_eos_token_ids": [
619
  1
620
  ],
621
  "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
622
  "processor": "arm",
623
+ "baseline_memory_mb": 3846.700552,
624
  "test_results": [
625
  {
626
  "max_tokens": 512,
627
  "tokens_generated": 512,
628
  "tokens_generated_source": "mlx-vlm token count",
629
  "tokens_generated_reliable": true,
630
+ "elapsed_seconds": 17.311166541010607,
631
+ "mean_tps": 32.76836342370835,
632
  "tps_source": "mlx-vlm generation_tps",
633
+ "peak_memory_mb": 5183.890113,
634
+ "memory_growth_mb": 1337.1895610000006,
635
  "repetition_rate": 0.0,
636
  "completed_naturally": false,
637
  "finish_reason": "length",
 
643
  "tokens_generated": 2048,
644
  "tokens_generated_source": "mlx-vlm token count",
645
  "tokens_generated_reliable": true,
646
+ "elapsed_seconds": 65.73962916599703,
647
+ "mean_tps": 32.0896585893278,
648
  "tps_source": "mlx-vlm generation_tps",
649
+ "peak_memory_mb": 5184.152257,
650
+ "memory_growth_mb": 1337.1895489999997,
651
+ "repetition_rate": 0.20496894409937888,
652
  "completed_naturally": false,
653
  "finish_reason": "length",
654
  "forced_minimum_tokens": 2048,
 
659
  "tokens_generated": 8192,
660
  "tokens_generated_source": "mlx-vlm token count",
661
  "tokens_generated_reliable": true,
662
+ "elapsed_seconds": 268.4810879580036,
663
+ "mean_tps": 30.743604031277776,
664
  "tps_source": "mlx-vlm generation_tps",
665
+ "peak_memory_mb": 5184.152257,
666
+ "memory_growth_mb": 1337.1895489999997,
667
+ "repetition_rate": 0.20496894409937888,
668
  "completed_naturally": false,
669
  "finish_reason": "length",
670
  "forced_minimum_tokens": 8192,
 
676
  "cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
677
  "tps_stable_over_length": true,
678
  "max_successful_tokens": 8192,
679
+ "max_repetition_rate": 0.20496894409937888
680
  },
681
  "pass_conditions": {
682
  "cache_bounded": true,
 
687
  },
688
  "sensitivity": {
689
  "baseline": {
690
+ "num_samples": 10,
691
+ "num_digit_samples": 10,
692
  "num_table_samples": 3,
693
+ "mean_cer": 0.20230911688823577,
694
+ "mean_digit_cer": 0.07236995104039168,
695
  "mean_table_score": 1.0,
696
+ "total_inference_time": 126.64344700002403,
697
+ "avg_time_per_image": 12.664344700002403
698
  },
699
  "groups": {
700
  "vision_encoder": {
 
848
  "vision_model.transformer.layers.9.self_attn.qkv_proj"
849
  ],
850
  "metrics": {
851
+ "num_samples": 10,
852
+ "num_digit_samples": 10,
853
  "num_table_samples": 3,
854
+ "mean_cer": 2.4745422809406823,
855
+ "mean_digit_cer": 1.7688242659575963,
856
  "mean_table_score": 0.0,
857
+ "total_inference_time": 310.5234005409875,
858
+ "avg_time_per_image": 31.05234005409875
859
  },
860
+ "cer_delta": 2.2722331640524467,
861
+ "digit_cer_delta": 1.6964543149172047,
862
  "table_score_degradation": 1.0,
863
  "sensitivity_rank": 1
864
  },
 
869
  "projector.layers"
870
  ],
871
  "metrics": {
872
+ "num_samples": 10,
873
+ "num_digit_samples": 10,
874
  "num_table_samples": 3,
875
+ "mean_cer": 0.20560582018493906,
876
+ "mean_digit_cer": 0.07236995104039168,
877
  "mean_table_score": 1.0,
878
+ "total_inference_time": 125.59270616796857,
879
+ "avg_time_per_image": 12.559270616796857
880
  },
881
+ "cer_delta": 0.003296703296703285,
882
+ "digit_cer_delta": 0.0,
883
  "table_score_degradation": 0.0,
884
+ "sensitivity_rank": 3
885
  },
886
  "token_embeddings": {
887
  "status": "success",
 
890
  "language_model.model.embed_tokens"
891
  ],
892
  "metrics": {
893
+ "num_samples": 10,
894
+ "num_digit_samples": 10,
895
  "num_table_samples": 3,
896
+ "mean_cer": 0.19852784584239075,
897
+ "mean_digit_cer": 0.07026468788249693,
898
  "mean_table_score": 1.0,
899
+ "total_inference_time": 125.69877116801217,
900
+ "avg_time_per_image": 12.569877116801218
901
  },
902
+ "cer_delta": -0.003781271045845025,
903
+ "digit_cer_delta": -0.002105263157894749,
904
  "table_score_degradation": 0.0,
905
  "sensitivity_rank": 10
906
  },
 
922
  "language_model.model.layers.9.self_attn.q_proj"
923
  ],
924
  "metrics": {
925
+ "num_samples": 10,
926
+ "num_digit_samples": 10,
927
  "num_table_samples": 3,
928
+ "mean_cer": 0.1810308435920253,
929
+ "mean_digit_cer": 0.08710679314565484,
930
  "mean_table_score": 1.0,
931
+ "total_inference_time": 126.60937520701555,
932
+ "avg_time_per_image": 12.660937520701555
933
  },
934
+ "cer_delta": -0.021278273296210487,
935
+ "digit_cer_delta": 0.01473684210526316,
936
  "table_score_degradation": 0.0,
937
  "sensitivity_rank": 11
938
  },
 
954
  "language_model.model.layers.9.self_attn.k_proj"
955
  ],
956
  "metrics": {
957
+ "num_samples": 10,
958
+ "num_digit_samples": 10,
959
  "num_table_samples": 3,
960
+ "mean_cer": 0.1995659706473317,
961
+ "mean_digit_cer": 0.07236995104039168,
962
  "mean_table_score": 1.0,
963
+ "total_inference_time": 125.29329520699685,
964
+ "avg_time_per_image": 12.529329520699685
965
  },
966
+ "cer_delta": -0.0027431462409040763,
967
+ "digit_cer_delta": 0.0,
968
  "table_score_degradation": 0.0,
969
  "sensitivity_rank": 9
970
  },
 
986
  "language_model.model.layers.9.self_attn.v_proj"
987
  ],
988
  "metrics": {
989
+ "num_samples": 10,
990
+ "num_digit_samples": 10,
991
  "num_table_samples": 3,
992
+ "mean_cer": 0.20121021578933468,
993
+ "mean_digit_cer": 0.07236995104039168,
994
  "mean_table_score": 1.0,
995
+ "total_inference_time": 126.87845074798679,
996
+ "avg_time_per_image": 12.687845074798679
997
  },
998
+ "cer_delta": -0.001098901098901095,
999
+ "digit_cer_delta": 0.0,
1000
  "table_score_degradation": 0.0,
1001
+ "sensitivity_rank": 6
1002
  },
1003
  "attention_o_proj": {
1004
  "status": "success",
 
1018
  "language_model.model.layers.9.self_attn.o_proj"
1019
  ],
1020
  "metrics": {
1021
+ "num_samples": 10,
1022
+ "num_digit_samples": 10,
1023
  "num_table_samples": 3,
1024
+ "mean_cer": 0.2058682899333709,
1025
+ "mean_digit_cer": 0.06383873929008568,
1026
  "mean_table_score": 1.0,
1027
+ "total_inference_time": 128.61252554102975,
1028
+ "avg_time_per_image": 12.861252554102975
1029
  },
1030
+ "cer_delta": 0.0035591730451351167,
1031
+ "digit_cer_delta": -0.008531211750305995,
1032
  "table_score_degradation": 0.0,
1033
+ "sensitivity_rank": 2
1034
  },
1035
  "dense_mlp": {
1036
  "status": "success",
 
1041
  "language_model.model.layers.0.mlp.up_proj"
1042
  ],
1043
  "metrics": {
1044
+ "num_samples": 10,
1045
+ "num_digit_samples": 10,
1046
  "num_table_samples": 3,
1047
+ "mean_cer": 0.20121021578933468,
1048
+ "mean_digit_cer": 0.07236995104039168,
1049
  "mean_table_score": 1.0,
1050
+ "total_inference_time": 127.19930095798918,
1051
+ "avg_time_per_image": 12.719930095798919
1052
  },
1053
+ "cer_delta": -0.001098901098901095,
1054
+ "digit_cer_delta": 0.0,
1055
  "table_score_degradation": 0.0,
1056
+ "sensitivity_rank": 7
1057
  },
1058
  "shared_experts": {
1059
  "status": "success",
 
1094
  "language_model.model.layers.9.mlp.shared_experts.up_proj"
1095
  ],
1096
  "metrics": {
1097
+ "num_samples": 10,
1098
+ "num_digit_samples": 10,
1099
  "num_table_samples": 3,
1100
+ "mean_cer": 0.20009091014419536,
1101
+ "mean_digit_cer": 0.08267594859241126,
1102
  "mean_table_score": 1.0,
1103
+ "total_inference_time": 128.06347554198874,
1104
+ "avg_time_per_image": 12.806347554198874
1105
  },
1106
+ "cer_delta": -0.002218206744040413,
1107
+ "digit_cer_delta": 0.01030599755201958,
1108
  "table_score_degradation": 0.0,
1109
+ "sensitivity_rank": 8
1110
  },
1111
  "routed_experts": {
1112
  "status": "success",
 
1147
  "language_model.model.layers.9.mlp.switch_mlp.up_proj"
1148
  ],
1149
  "metrics": {
1150
+ "num_samples": 10,
1151
+ "num_digit_samples": 10,
1152
  "num_table_samples": 3,
1153
+ "mean_cer": 0.2033268371469385,
1154
+ "mean_digit_cer": 0.06899173806609546,
1155
  "mean_table_score": 1.0,
1156
+ "total_inference_time": 125.24760646000504,
1157
+ "avg_time_per_image": 12.524760646000505
1158
  },
1159
+ "cer_delta": 0.0010177202587027256,
1160
+ "digit_cer_delta": -0.0033782129742962186,
1161
  "table_score_degradation": 0.0,
1162
+ "sensitivity_rank": 5
1163
  },
1164
  "lm_head": {
1165
  "status": "success",
 
1168
  "language_model.lm_head"
1169
  ],
1170
  "metrics": {
1171
+ "num_samples": 10,
1172
+ "num_digit_samples": 10,
1173
  "num_table_samples": 3,
1174
+ "mean_cer": 0.20560582018493906,
1175
+ "mean_digit_cer": 0.07236995104039168,
1176
  "mean_table_score": 1.0,
1177
+ "total_inference_time": 122.19146683299914,
1178
+ "avg_time_per_image": 12.219146683299915
1179
  },
1180
+ "cer_delta": 0.003296703296703285,
1181
+ "digit_cer_delta": 0.0,
1182
  "table_score_degradation": 0.0,
1183
+ "sensitivity_rank": 4
1184
  }
1185
  }
1186
  },
1187
  "calibration": {
1188
+ "schema_version": 3,
1189
+ "created_at": "2026-07-21T05:26:12.430585+00:00",
1190
  "target_pattern": "language_model.lm_head",
1191
  "selection_policy": "fastest candidate passing existing quality and throughput limits",
1192
  "thresholds": {
1193
+ "max_cer_delta_vs_bf16": 0.015,
1194
  "max_cer_delta_vs_reference": 0.005,
1195
  "max_digit_cer_delta_vs_bf16": 0.01,
1196
  "max_table_score_degradation_vs_bf16": 0.01,
1197
  "min_tps_ratio_vs_reference": 0.9,
1198
+ "max_weight_size_gb": 4.5,
1199
+ "min_rswa_tokens": 8192,
1200
+ "max_rswa_repetition_rate": 0.25
1201
  },
1202
  "experiments": [
1203
  {
1204
  "label": "bf16-head",
1205
  "precision": "bfloat16",
1206
+ "passed": true,
1207
  "checks": {
1208
  "cer": true,
1209
  "digit_cer": true,
1210
  "table_score": true,
1211
+ "throughput": true
1212
  },
1213
  "metrics": {
1214
+ "mean_cer": 0.19010066585760363,
1215
+ "mean_digit_cer": 0.06278610771113831,
1216
  "mean_table_score": 1.0,
1217
+ "mean_tps": 38.770309481806514
1218
  },
1219
  "deltas": {
1220
+ "cer_vs_bf16": -0.012208451030632145,
1221
+ "digit_cer_vs_bf16": -0.00958384332925337,
1222
  "table_degradation_vs_bf16": 0.0,
1223
+ "tps_ratio_vs_reference": 0.9595978674779929
1224
  }
1225
  },
1226
  {
 
1228
  "precision": "mxfp8",
1229
  "passed": false,
1230
  "checks": {
1231
+ "cer": true,
1232
+ "digit_cer": false,
1233
  "table_score": true,
1234
  "throughput": true
1235
  },
1236
  "metrics": {
1237
+ "mean_cer": 0.21607021200652196,
1238
+ "mean_digit_cer": 0.1052422480620155,
1239
  "mean_table_score": 1.0,
1240
+ "mean_tps": 41.326731942543184
1241
  },
1242
  "deltas": {
1243
+ "cer_vs_bf16": 0.013761095118286187,
1244
+ "digit_cer_vs_bf16": 0.03287229702162382,
1245
  "table_degradation_vs_bf16": 0.0,
1246
+ "tps_ratio_vs_reference": 1.0228714800563738
1247
  }
1248
  },
1249
  {
 
1257
  "throughput": true
1258
  },
1259
  "metrics": {
1260
+ "mean_cer": 0.19105830313811087,
1261
+ "mean_digit_cer": 0.07857558139534884,
1262
  "mean_table_score": 1.0,
1263
+ "mean_tps": 39.58614991640946
1264
  },
1265
  "deltas": {
1266
+ "cer_vs_bf16": -0.011250813750124905,
1267
+ "digit_cer_vs_bf16": 0.0062056303549571645,
1268
  "table_degradation_vs_bf16": 0.0,
1269
+ "tps_ratio_vs_reference": 0.979790606502031
1270
  }
1271
  }
1272
  ],
 
1276
  },
1277
  "precision_overrides": {
1278
  "language_model.lm_head": "affine8"
1279
+ },
1280
+ "input_artifacts": {
1281
+ "calibration_baseline_accuracy": {
1282
+ "filename": "calibration_baseline_accuracy.json",
1283
+ "size": 5680,
1284
+ "sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
1285
+ },
1286
+ "calibration_reference_performance": {
1287
+ "filename": "calibration_reference_performance.json",
1288
+ "size": 2271,
1289
+ "sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
1290
+ },
1291
+ "calibration_bfloat16_accuracy": {
1292
+ "filename": "calibration_bfloat16_accuracy.json",
1293
+ "size": 5678,
1294
+ "sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
1295
+ },
1296
+ "calibration_bfloat16_performance": {
1297
+ "filename": "calibration_bfloat16_performance.json",
1298
+ "size": 2244,
1299
+ "sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
1300
+ },
1301
+ "calibration_mxfp8_accuracy": {
1302
+ "filename": "calibration_mxfp8_accuracy.json",
1303
+ "size": 5689,
1304
+ "sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
1305
+ },
1306
+ "calibration_mxfp8_performance": {
1307
+ "filename": "calibration_mxfp8_performance.json",
1308
+ "size": 2252,
1309
+ "sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
1310
+ },
1311
+ "calibration_affine8_accuracy": {
1312
+ "filename": "calibration_affine8_accuracy.json",
1313
+ "size": 5657,
1314
+ "sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
1315
+ },
1316
+ "calibration_affine8_performance": {
1317
+ "filename": "calibration_affine8_performance.json",
1318
+ "size": 2247,
1319
+ "sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
1320
+ }
1321
+ },
1322
+ "dataset": {
1323
+ "name": "calibration",
1324
+ "num_samples": 10,
1325
+ "num_digit_samples": 10,
1326
+ "num_cjk_samples": 3,
1327
+ "num_table_samples": 3,
1328
+ "category_counts": {
1329
+ "cjk_document": 3,
1330
+ "english_document": 2,
1331
+ "low_quality_scan": 2,
1332
+ "table_numeric": 3
1333
+ },
1334
+ "manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
1335
+ "content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
1336
+ "files": [
1337
+ {
1338
+ "path": "images/0001.png",
1339
+ "size": 243645,
1340
+ "sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
1341
+ },
1342
+ {
1343
+ "path": "ground_truth/0001.txt",
1344
+ "size": 1346,
1345
+ "sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
1346
+ },
1347
+ {
1348
+ "path": "images/0002.png",
1349
+ "size": 208699,
1350
+ "sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
1351
+ },
1352
+ {
1353
+ "path": "ground_truth/0002.txt",
1354
+ "size": 838,
1355
+ "sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
1356
+ },
1357
+ {
1358
+ "path": "images/0003.png",
1359
+ "size": 40039,
1360
+ "sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
1361
+ },
1362
+ {
1363
+ "path": "ground_truth/0003.txt",
1364
+ "size": 459,
1365
+ "sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
1366
+ },
1367
+ {
1368
+ "path": "images/0004.png",
1369
+ "size": 40356,
1370
+ "sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
1371
+ },
1372
+ {
1373
+ "path": "ground_truth/0004.txt",
1374
+ "size": 459,
1375
+ "sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
1376
+ },
1377
+ {
1378
+ "path": "images/0005.png",
1379
+ "size": 40650,
1380
+ "sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
1381
+ },
1382
+ {
1383
+ "path": "ground_truth/0005.txt",
1384
+ "size": 459,
1385
+ "sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
1386
+ },
1387
+ {
1388
+ "path": "images/0006.png",
1389
+ "size": 46036,
1390
+ "sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
1391
+ },
1392
+ {
1393
+ "path": "ground_truth/0006.txt",
1394
+ "size": 191,
1395
+ "sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
1396
+ },
1397
+ {
1398
+ "path": "images/0007.png",
1399
+ "size": 41828,
1400
+ "sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
1401
+ },
1402
+ {
1403
+ "path": "ground_truth/0007.txt",
1404
+ "size": 195,
1405
+ "sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
1406
+ },
1407
+ {
1408
+ "path": "images/0008.png",
1409
+ "size": 20975,
1410
+ "sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
1411
+ },
1412
+ {
1413
+ "path": "ground_truth/0008.txt",
1414
+ "size": 201,
1415
+ "sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
1416
+ },
1417
+ {
1418
+ "path": "images/0009.png",
1419
+ "size": 487313,
1420
+ "sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
1421
+ },
1422
+ {
1423
+ "path": "ground_truth/0009.txt",
1424
+ "size": 121,
1425
+ "sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
1426
+ },
1427
+ {
1428
+ "path": "images/0010.png",
1429
+ "size": 515820,
1430
+ "sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
1431
+ },
1432
+ {
1433
+ "path": "ground_truth/0010.txt",
1434
+ "size": 121,
1435
+ "sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
1436
+ }
1437
+ ]
1438
  }
1439
  },
1440
  "precision_map": {
1441
  "vision_model": "bfloat16",
1442
+ "projector": "bfloat16",
1443
  "language_model.model.embed_tokens": "mxfp8",
1444
  "language_model.model.norm": "bfloat16",
1445
  "language_model.lm_head": "affine8",
 
1471
  "vision_encoder": {
1472
  "precision": "bfloat16",
1473
  "reasons": [
1474
+ "cer_delta=2.272233",
1475
+ "digit_cer_delta=1.696454",
1476
  "table_score_degradation=1.000000",
1477
  "protected OCR-critical group"
1478
  ]
1479
  },
1480
  "vision_projector": {
1481
+ "precision": "bfloat16",
1482
  "reasons": [
1483
+ "protected unless all measured quality deltas are non-degrading"
1484
  ]
1485
  },
1486
  "token_embeddings": {
 
1548
  }
1549
  },
1550
  "artifacts": {
 
 
 
 
 
1551
  "reference_accuracy": {
1552
  "filename": "reference_accuracy.json",
1553
+ "size": 6664,
1554
+ "sha256": "2cd131108b70f0252995c0c096bd203bd7f7e6d06ce8921e1585b8a0a4523ad7"
1555
  },
1556
+ "generated_precision_map": {
1557
+ "filename": "generated_precision_map.json",
1558
+ "size": 3337,
1559
+ "sha256": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75"
 
 
 
 
 
1560
  },
1561
  "candidate_performance": {
1562
  "filename": "candidate_performance.json",
1563
+ "size": 2261,
1564
+ "sha256": "71034ab96428b1ff5f7d700166aa393bacca91a465bb265bfe33af96d91dde52"
 
 
 
 
 
1565
  },
1566
  "sensitivity_results": {
1567
  "filename": "sensitivity_results.json",
1568
+ "size": 26432,
1569
+ "sha256": "170e57762b46a880360c3c6b89e2ed96e6ceb08454541a0baa7432e53f60ce49"
1570
  },
1571
  "calibration_results": {
1572
  "filename": "calibration_results.json",
1573
+ "size": 8019,
1574
+ "sha256": "b077712a0521f50e0b8c6ba1ca20e66d0ee64f750d6a545a1b3ab378af50adf7"
1575
  },
1576
+ "candidate_accuracy": {
1577
+ "filename": "candidate_accuracy.json",
1578
+ "size": 6562,
1579
+ "sha256": "60bbb1f0ac5ea87e64d57889c5ade08aa5341e9558c530c009cb7fa546e68c25"
1580
+ },
1581
+ "bf16_accuracy": {
1582
+ "filename": "bf16_accuracy.json",
1583
+ "size": 6499,
1584
+ "sha256": "04289ab8cd0ee4019a341eaab83617eeca3cca99f2f96f236cb8725036789659"
1585
+ },
1586
+ "reference_performance": {
1587
+ "filename": "reference_performance.json",
1588
+ "size": 2271,
1589
+ "sha256": "1ac6b646a090cd052eea48ff50cc96d666204e986273a98f43daf263cecb270d"
1590
  },
1591
  "provenance": {
1592
  "filename": "provenance.json",
1593
+ "size": 9280,
1594
+ "sha256": "958930c6a480798922a88287ba53d75de13df2fa7710d387c7138bbf300601e8"
1595
+ },
1596
+ "candidate_rswa": {
1597
+ "filename": "candidate_rswa.json",
1598
+ "size": 2483,
1599
+ "sha256": "1516b35616be2947d5595a3c9779f7b960adee4ad472336f50f5de88a151ebe0"
1600
+ },
1601
+ "calibration_affine8_accuracy": {
1602
+ "filename": "calibration_affine8_accuracy.json",
1603
+ "size": 5657,
1604
+ "sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
1605
+ },
1606
+ "calibration_bfloat16_accuracy": {
1607
+ "filename": "calibration_bfloat16_accuracy.json",
1608
+ "size": 5678,
1609
+ "sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
1610
+ },
1611
+ "calibration_mxfp8_performance": {
1612
+ "filename": "calibration_mxfp8_performance.json",
1613
+ "size": 2252,
1614
+ "sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
1615
+ },
1616
+ "calibration_reference_performance": {
1617
+ "filename": "calibration_reference_performance.json",
1618
+ "size": 2271,
1619
+ "sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
1620
+ },
1621
+ "calibration_mxfp8_accuracy": {
1622
+ "filename": "calibration_mxfp8_accuracy.json",
1623
+ "size": 5689,
1624
+ "sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
1625
+ },
1626
+ "calibration_affine8_performance": {
1627
+ "filename": "calibration_affine8_performance.json",
1628
+ "size": 2247,
1629
+ "sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
1630
+ },
1631
+ "calibration_bfloat16_performance": {
1632
+ "filename": "calibration_bfloat16_performance.json",
1633
+ "size": 2244,
1634
+ "sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
1635
+ },
1636
+ "calibration_baseline_accuracy": {
1637
+ "filename": "calibration_baseline_accuracy.json",
1638
+ "size": 5680,
1639
+ "sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
1640
  }
1641
  },
1642
  "gates": [
1643
  {
1644
  "name": "weights_are_distinct",
1645
  "passed": true,
1646
+ "actual": "5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549",
1647
  "limit": "different from 439c6acfe5e277537dfe3368b94a145a6b0da4c39e4bd00582977d030f292ad1",
1648
  "detail": "Candidate aggregate digest must differ from the Sahil reference"
1649
  },
1650
  {
1651
  "name": "weight_size_gb",
1652
  "passed": true,
1653
+ "actual": 3.5815872186794877,
1654
  "limit": 4.5,
1655
  "detail": "Candidate Safetensors size"
1656
  },
 
1663
  "mxfp8": true,
1664
  "sliding_window": true,
1665
  "processor_class": true,
1666
+ "sft_format": true,
1667
+ "precision_map": true,
1668
+ "quantization_summary": true,
1669
+ "protected_modules_preserved": true
1670
  },
1671
  "limit": true,
1672
  "detail": "Native Unlimited-OCR, MXFP8, and R-SWA metadata"
1673
  },
1674
+ {
1675
+ "name": "candidate_source_provenance",
1676
+ "passed": true,
1677
+ "actual": {
1678
+ "model": "Unlimited-OCR",
1679
+ "revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead"
1680
+ },
1681
+ "limit": {
1682
+ "model": [
1683
+ "baidu/Unlimited-OCR",
1684
+ "Unlimited-OCR"
1685
+ ],
1686
+ "revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead"
1687
+ },
1688
+ "detail": "Converted metadata must bind the exact BF16 source commit"
1689
+ },
1690
  {
1691
  "name": "evaluation_coverage",
1692
  "passed": true,
 
1700
  "detail": "Dataset must cover ordinary text, digits, CJK, and tables"
1701
  },
1702
  {
1703
+ "name": "held_out_evaluation_dataset",
1704
+ "passed": true,
1705
+ "actual": {
1706
+ "passed": true,
1707
+ "calibration_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
1708
+ "evaluation_digest": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
1709
+ "overlapping_image_sha256": [],
1710
+ "overlapping_ground_truth_sha256": []
1711
+ },
1712
+ "limit": "distinct dataset digests and no shared image or ground-truth hashes",
1713
+ "detail": "Final quality evidence must not reuse model-selection samples"
1714
+ },
1715
+ {
1716
+ "name": "accuracy_aggregates_recomputed",
1717
  "passed": true,
1718
  "actual": [
1719
+ true,
1720
+ true,
1721
+ true
1722
+ ],
1723
+ "limit": [
1724
+ true,
1725
+ true,
1726
+ true
1727
  ],
1728
+ "detail": "Every reported accuracy aggregate must recompute from per-file rows"
1729
+ },
1730
+ {
1731
+ "name": "same_evaluation_samples",
1732
+ "passed": true,
1733
+ "actual": {
1734
+ "counts": [
1735
+ 12,
1736
+ 12,
1737
+ 12
1738
+ ],
1739
+ "file_counts": [
1740
+ 12,
1741
+ 12,
1742
+ 12
1743
+ ]
1744
+ },
1745
+ "limit": {
1746
+ "count": 12,
1747
+ "files": [
1748
+ "0001.png",
1749
+ "0002.png",
1750
+ "0003.png",
1751
+ "0004.png",
1752
+ "0005.png",
1753
+ "0006.png",
1754
+ "0007.png",
1755
+ "0008.png",
1756
+ "0009.png",
1757
+ "0010.png",
1758
+ "0011.png",
1759
+ "0012.png"
1760
+ ]
1761
+ },
1762
+ "detail": "All three checkpoints must run every identical evaluation file"
1763
+ },
1764
+ {
1765
+ "name": "model_identities",
1766
+ "passed": true,
1767
+ "actual": {
1768
+ "bf16": "Unlimited-OCR",
1769
+ "reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
1770
+ "candidate": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
1771
+ "reference_performance": "sahilchachra/unlimited-ocr-mxfp8-mlx",
1772
+ "candidate_performance": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
1773
+ "rswa": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
1774
+ },
1775
+ "limit": {
1776
+ "bf16": "Unlimited-OCR",
1777
+ "reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
1778
+ "candidate": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
1779
+ },
1780
+ "detail": "Accuracy, performance, and R-SWA evidence must identify exact models"
1781
+ },
1782
+ {
1783
+ "name": "immutable_model_revisions",
1784
+ "passed": true,
1785
+ "actual": {
1786
+ "bf16_accuracy": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
1787
+ "reference_accuracy": "55b8031a6c867de675279d9604e38cc94b9882a4",
1788
+ "candidate_accuracy": null,
1789
+ "reference_performance": "55b8031a6c867de675279d9604e38cc94b9882a4",
1790
+ "candidate_performance": null
1791
+ },
1792
+ "limit": {
1793
+ "bf16_accuracy": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
1794
+ "reference_accuracy": "55b8031a6c867de675279d9604e38cc94b9882a4",
1795
+ "candidate_accuracy": null,
1796
+ "reference_performance": "55b8031a6c867de675279d9604e38cc94b9882a4",
1797
+ "candidate_performance": null
1798
+ },
1799
+ "detail": "Remote source and reference evidence must name exact Hub commits"
1800
+ },
1801
+ {
1802
+ "name": "same_accuracy_recipe",
1803
+ "passed": true,
1804
+ "actual": {
1805
+ "prompt": [
1806
+ "<image>document parsing.",
1807
+ "<image>document parsing.",
1808
+ "<image>document parsing."
1809
+ ],
1810
+ "max_tokens": [
1811
+ 1024,
1812
+ 1024,
1813
+ 1024
1814
+ ],
1815
+ "profile": [
1816
+ "accurate",
1817
+ "accurate",
1818
+ "accurate"
1819
+ ],
1820
+ "generation_settings": [
1821
+ {
1822
+ "temperature": 0.0,
1823
+ "top_p": 1.0,
1824
+ "repetition_penalty": 1.0,
1825
+ "no_repeat_ngram_size": 35,
1826
+ "ngram_window": 128
1827
+ },
1828
+ {
1829
+ "temperature": 0.0,
1830
+ "top_p": 1.0,
1831
+ "repetition_penalty": 1.0,
1832
+ "no_repeat_ngram_size": 35,
1833
+ "ngram_window": 128
1834
+ },
1835
+ {
1836
+ "temperature": 0.0,
1837
+ "top_p": 1.0,
1838
+ "repetition_penalty": 1.0,
1839
+ "no_repeat_ngram_size": 35,
1840
+ "ngram_window": 128
1841
+ }
1842
+ ]
1843
+ },
1844
+ "limit": "identical official MLX OCR recipe",
1845
+ "detail": "Accuracy runs must use the same deterministic OCR generation settings"
1846
  },
1847
  {
1848
  "name": "candidate_cer_vs_bf16",
1849
  "passed": true,
1850
+ "actual": 0.011118653228923858,
1851
+ "limit": 0.015,
1852
  "detail": "Candidate minus BF16 absolute mean CER"
1853
  },
1854
  {
1855
  "name": "candidate_cer_vs_reference",
1856
  "passed": true,
1857
+ "actual": -0.691050362380767,
1858
  "limit": 0.005,
1859
  "detail": "Candidate minus Sahil-reference absolute mean CER"
1860
  },
1861
  {
1862
  "name": "candidate_digit_cer_vs_bf16",
1863
  "passed": true,
1864
+ "actual": -0.0013706317359285375,
1865
  "limit": 0.01,
1866
  "detail": "Candidate minus BF16 digit CER"
1867
  },
 
1872
  "limit": 0.01,
1873
  "detail": "BF16 minus candidate mean table score"
1874
  },
1875
+ {
1876
+ "name": "performance_aggregates_recomputed",
1877
+ "passed": true,
1878
+ "actual": {
1879
+ "reference": true,
1880
+ "candidate": true
1881
+ },
1882
+ "limit": {
1883
+ "reference": true,
1884
+ "candidate": true
1885
+ },
1886
+ "detail": "Performance means must recompute from complete benchmark runs"
1887
+ },
1888
+ {
1889
+ "name": "same_performance_setup",
1890
+ "passed": true,
1891
+ "actual": {
1892
+ "image_path": [
1893
+ "test_invoice.png",
1894
+ "test_invoice.png"
1895
+ ],
1896
+ "prompt": [
1897
+ "<image>document parsing.",
1898
+ "<image>document parsing."
1899
+ ],
1900
+ "max_tokens": [
1901
+ 256,
1902
+ 256
1903
+ ],
1904
+ "num_warmup": [
1905
+ 1,
1906
+ 1
1907
+ ],
1908
+ "num_runs": [
1909
+ 3,
1910
+ 3
1911
+ ],
1912
+ "system": [
1913
+ {
1914
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
1915
+ "processor": "arm",
1916
+ "python_version": "3.14.6",
1917
+ "machine": "arm64",
1918
+ "mlx_version": "0.32.0",
1919
+ "mlx_vlm_version": "0.6.6",
1920
+ "chip": "Apple M3 Max",
1921
+ "total_memory_gb": 128.0
1922
+ },
1923
+ {
1924
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
1925
+ "processor": "arm",
1926
+ "python_version": "3.14.6",
1927
+ "machine": "arm64",
1928
+ "mlx_version": "0.32.0",
1929
+ "mlx_vlm_version": "0.6.6",
1930
+ "chip": "Apple M3 Max",
1931
+ "total_memory_gb": 128.0
1932
+ }
1933
+ ]
1934
+ },
1935
+ "limit": "identical setup with at least three complete runs",
1936
+ "detail": "Performance comparisons must use the same host and benchmark recipe"
1937
+ },
1938
  {
1939
  "name": "candidate_tps_vs_reference",
1940
  "passed": true,
1941
+ "actual": 1.001955642681865,
1942
  "limit": 0.9,
1943
  "detail": "Candidate decode throughput divided by Sahil-reference throughput"
1944
  },
 
1946
  "name": "rswa_8k_bounded",
1947
  "passed": true,
1948
  "actual": {
1949
+ "pass_conditions": {
1950
+ "cache_bounded": true,
1951
+ "tps_stable": true,
1952
+ "8k_test_passed": true
1953
+ },
1954
+ "tokens": 8192,
1955
+ "repetition_rate": 0.20496894409937888
1956
  },
1957
+ "limit": {
1958
+ "min_tokens": 8192,
1959
+ "max_repetition_rate": 0.25
1960
+ },
1961
+ "detail": "8K generation must have bounded cache, stable throughput, and repetition"
1962
+ },
1963
+ {
1964
+ "name": "provenance_matches_release",
1965
+ "passed": true,
1966
+ "actual": {
1967
+ "source": "baidu/Unlimited-OCR",
1968
+ "reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
1969
+ "target": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
1970
+ "source_config": {
1971
+ "path": "config.json",
1972
+ "sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9"
1973
+ },
1974
+ "source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
1975
+ "reference_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
1976
+ "dataset_digest": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
1977
+ "calibration_dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb"
1978
+ },
1979
+ "limit": {
1980
+ "source": "baidu/Unlimited-OCR",
1981
+ "reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
1982
+ "target": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
1983
+ "source_config_sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9",
1984
+ "source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
1985
+ "reference_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
1986
+ "dataset_digest": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
1987
+ "calibration_dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb"
1988
+ },
1989
+ "detail": "Provenance must bind the source, selection/evaluation datasets, and target"
1990
+ },
1991
+ {
1992
+ "name": "calibration_recomputed",
1993
+ "passed": true,
1994
+ "actual": {
1995
+ "selected": {
1996
+ "label": "affine8-head",
1997
+ "precision": "affine8"
1998
+ },
1999
+ "models": [
2000
+ "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8",
2001
+ "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16",
2002
+ "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8"
2003
+ ],
2004
+ "dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
2005
+ "input_artifacts": {
2006
+ "calibration_baseline_accuracy": {
2007
+ "filename": "calibration_baseline_accuracy.json",
2008
+ "size": 5680,
2009
+ "sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
2010
+ },
2011
+ "calibration_reference_performance": {
2012
+ "filename": "calibration_reference_performance.json",
2013
+ "size": 2271,
2014
+ "sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
2015
+ },
2016
+ "calibration_bfloat16_accuracy": {
2017
+ "filename": "calibration_bfloat16_accuracy.json",
2018
+ "size": 5678,
2019
+ "sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
2020
+ },
2021
+ "calibration_bfloat16_performance": {
2022
+ "filename": "calibration_bfloat16_performance.json",
2023
+ "size": 2244,
2024
+ "sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
2025
+ },
2026
+ "calibration_mxfp8_accuracy": {
2027
+ "filename": "calibration_mxfp8_accuracy.json",
2028
+ "size": 5689,
2029
+ "sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
2030
+ },
2031
+ "calibration_mxfp8_performance": {
2032
+ "filename": "calibration_mxfp8_performance.json",
2033
+ "size": 2252,
2034
+ "sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
2035
+ },
2036
+ "calibration_affine8_accuracy": {
2037
+ "filename": "calibration_affine8_accuracy.json",
2038
+ "size": 5657,
2039
+ "sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
2040
+ },
2041
+ "calibration_affine8_performance": {
2042
+ "filename": "calibration_affine8_performance.json",
2043
+ "size": 2247,
2044
+ "sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
2045
+ }
2046
+ }
2047
+ },
2048
+ "limit": "content-addressed inputs and fastest passing experiment",
2049
+ "detail": "Calibration claims must recompute from raw benchmark evidence"
2050
+ },
2051
+ {
2052
+ "name": "sensitivity_matches_calibration_dataset",
2053
+ "passed": true,
2054
+ "actual": {
2055
+ "dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
2056
+ "baseline_metrics_match": true
2057
+ },
2058
+ "limit": {
2059
+ "dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
2060
+ "baseline_metrics_match": true
2061
+ },
2062
+ "detail": "Sensitivity decisions must use the recorded selection dataset and baseline"
2063
+ },
2064
+ {
2065
+ "name": "candidate_precision_map_matches_evidence",
2066
+ "passed": true,
2067
+ "actual": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75",
2068
+ "limit": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75",
2069
+ "detail": "The executable candidate precision map must equal approved evidence"
2070
+ },
2071
+ {
2072
+ "name": "precision_map_reproducible",
2073
+ "passed": true,
2074
+ "actual": "c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a",
2075
+ "limit": "c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a",
2076
+ "detail": "Sensitivity and calibration evidence must reproduce the executable map"
2077
  }
2078
  ],
2079
  "environment": {
release/sensitivity_results.json CHANGED
@@ -1,16 +1,134 @@
1
  {
2
  "baseline": {
3
- "num_samples": 12,
4
- "num_digit_samples": 12,
5
  "num_table_samples": 3,
6
- "mean_cer": 0.2391877132924964,
7
- "mean_digit_cer": 0.08733996757027829,
8
  "mean_table_score": 1.0,
9
- "total_inference_time": 37.96379516503657,
10
- "avg_time_per_image": 3.163649597086381
11
  },
12
  "model_path": "baidu/Unlimited-OCR",
13
- "eval_dir": "eval_dataset",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  "prompt": "<image>document parsing.",
15
  "max_tokens": 1024,
16
  "groups_tested": [
@@ -178,17 +296,17 @@
178
  "vision_model.transformer.layers.9.self_attn.qkv_proj"
179
  ],
180
  "metrics": {
181
- "num_samples": 12,
182
- "num_digit_samples": 12,
183
  "num_table_samples": 3,
184
- "mean_cer": 2.6628739674476654,
185
- "mean_digit_cer": 1.445363129900987,
186
  "mean_table_score": 0.0,
187
- "total_inference_time": 82.4734385000047,
188
- "avg_time_per_image": 6.872786541667058
189
  },
190
- "cer_delta": 2.423686254155169,
191
- "digit_cer_delta": 1.3580231623307086,
192
  "table_score_degradation": 1.0,
193
  "sensitivity_rank": 1
194
  },
@@ -199,19 +317,19 @@
199
  "projector.layers"
200
  ],
201
  "metrics": {
202
- "num_samples": 12,
203
- "num_digit_samples": 12,
204
  "num_table_samples": 3,
205
- "mean_cer": 0.23591319611894124,
206
- "mean_digit_cer": 0.08425354781719185,
207
  "mean_table_score": 1.0,
208
- "total_inference_time": 37.98282545800612,
209
- "avg_time_per_image": 3.165235454833843
210
  },
211
- "cer_delta": -0.003274517173555175,
212
- "digit_cer_delta": -0.0030864197530864335,
213
  "table_score_degradation": 0.0,
214
- "sensitivity_rank": 7
215
  },
216
  "token_embeddings": {
217
  "status": "success",
@@ -220,17 +338,17 @@
220
  "language_model.model.embed_tokens"
221
  ],
222
  "metrics": {
223
- "num_samples": 12,
224
- "num_digit_samples": 12,
225
  "num_table_samples": 3,
226
- "mean_cer": 0.2326891302977425,
227
- "mean_digit_cer": 0.08249916185227958,
228
  "mean_table_score": 1.0,
229
- "total_inference_time": 38.66650962494896,
230
- "avg_time_per_image": 3.222209135412413
231
  },
232
- "cer_delta": -0.006498582994753921,
233
- "digit_cer_delta": -0.004840805717998706,
234
  "table_score_degradation": 0.0,
235
  "sensitivity_rank": 10
236
  },
@@ -252,17 +370,17 @@
252
  "language_model.model.layers.9.self_attn.q_proj"
253
  ],
254
  "metrics": {
255
- "num_samples": 12,
256
- "num_digit_samples": 12,
257
  "num_table_samples": 3,
258
- "mean_cer": 0.21507560490000377,
259
- "mean_digit_cer": 0.09653424957157782,
260
  "mean_table_score": 1.0,
261
- "total_inference_time": 38.38538125100604,
262
- "avg_time_per_image": 3.19878177091717
263
  },
264
- "cer_delta": -0.024112108392492643,
265
- "digit_cer_delta": 0.009194282001299528,
266
  "table_score_degradation": 0.0,
267
  "sensitivity_rank": 11
268
  },
@@ -284,17 +402,17 @@
284
  "language_model.model.layers.9.self_attn.k_proj"
285
  ],
286
  "metrics": {
287
- "num_samples": 12,
288
- "num_digit_samples": 12,
289
  "num_table_samples": 3,
290
- "mean_cer": 0.23424214947533464,
291
- "mean_digit_cer": 0.08425354781719185,
292
  "mean_table_score": 1.0,
293
- "total_inference_time": 38.175402873021085,
294
- "avg_time_per_image": 3.181283572751757
295
  },
296
- "cer_delta": -0.004945563817161769,
297
- "digit_cer_delta": -0.0030864197530864335,
298
  "table_score_degradation": 0.0,
299
  "sensitivity_rank": 9
300
  },
@@ -316,19 +434,19 @@
316
  "language_model.model.layers.9.self_attn.v_proj"
317
  ],
318
  "metrics": {
319
- "num_samples": 12,
320
- "num_digit_samples": 12,
321
  "num_table_samples": 3,
322
- "mean_cer": 0.23642534441989824,
323
- "mean_digit_cer": 0.08425354781719185,
324
  "mean_table_score": 1.0,
325
- "total_inference_time": 41.72621229101787,
326
- "avg_time_per_image": 3.477184357584823
327
  },
328
- "cer_delta": -0.0027623688725981754,
329
- "digit_cer_delta": -0.0030864197530864335,
330
  "table_score_degradation": 0.0,
331
- "sensitivity_rank": 5
332
  },
333
  "attention_o_proj": {
334
  "status": "success",
@@ -348,19 +466,19 @@
348
  "language_model.model.layers.9.self_attn.o_proj"
349
  ],
350
  "metrics": {
351
- "num_samples": 12,
352
- "num_digit_samples": 12,
353
  "num_table_samples": 3,
354
- "mean_cer": 0.2359826017166606,
355
- "mean_digit_cer": 0.07714420469193685,
356
  "mean_table_score": 1.0,
357
- "total_inference_time": 41.9442965429771,
358
- "avg_time_per_image": 3.4953580452480915
359
  },
360
- "cer_delta": -0.003205111575835806,
361
- "digit_cer_delta": -0.010195762878341438,
362
  "table_score_degradation": 0.0,
363
- "sensitivity_rank": 6
364
  },
365
  "dense_mlp": {
366
  "status": "success",
@@ -371,19 +489,19 @@
371
  "language_model.model.layers.0.mlp.up_proj"
372
  ],
373
  "metrics": {
374
- "num_samples": 12,
375
- "num_digit_samples": 12,
376
  "num_table_samples": 3,
377
- "mean_cer": 0.23568756434998817,
378
- "mean_digit_cer": 0.08425354781719185,
379
  "mean_table_score": 1.0,
380
- "total_inference_time": 41.424109207015135,
381
- "avg_time_per_image": 3.4520091005845948
382
  },
383
- "cer_delta": -0.0035001489425082466,
384
- "digit_cer_delta": -0.0030864197530864335,
385
  "table_score_degradation": 0.0,
386
- "sensitivity_rank": 8
387
  },
388
  "shared_experts": {
389
  "status": "success",
@@ -424,19 +542,19 @@
424
  "language_model.model.layers.9.mlp.shared_experts.up_proj"
425
  ],
426
  "metrics": {
427
- "num_samples": 12,
428
- "num_digit_samples": 12,
429
  "num_table_samples": 3,
430
- "mean_cer": 0.23649792801080774,
431
- "mean_digit_cer": 0.09592829886362793,
432
  "mean_table_score": 1.0,
433
- "total_inference_time": 37.84185174998129,
434
- "avg_time_per_image": 3.153487645831774
435
  },
436
- "cer_delta": -0.0026897852816886725,
437
- "digit_cer_delta": 0.00858833129334964,
438
  "table_score_degradation": 0.0,
439
- "sensitivity_rank": 4
440
  },
441
  "routed_experts": {
442
  "status": "success",
@@ -477,19 +595,19 @@
477
  "language_model.model.layers.9.mlp.switch_mlp.up_proj"
478
  ],
479
  "metrics": {
480
- "num_samples": 12,
481
- "num_digit_samples": 12,
482
  "num_table_samples": 3,
483
- "mean_cer": 0.23911822127011442,
484
- "mean_digit_cer": 0.08452479009169811,
485
  "mean_table_score": 1.0,
486
- "total_inference_time": 36.668351295011234,
487
- "avg_time_per_image": 3.0556959412509364
488
  },
489
- "cer_delta": -6.949202238198948e-05,
490
- "digit_cer_delta": -0.0028151774785801775,
491
  "table_score_degradation": 0.0,
492
- "sensitivity_rank": 3
493
  },
494
  "lm_head": {
495
  "status": "success",
@@ -498,19 +616,19 @@
498
  "language_model.lm_head"
499
  ],
500
  "metrics": {
501
- "num_samples": 12,
502
- "num_digit_samples": 12,
503
  "num_table_samples": 3,
504
- "mean_cer": 0.25282999056583605,
505
- "mean_digit_cer": 0.12301897991595728,
506
  "mean_table_score": 1.0,
507
- "total_inference_time": 37.152046875009546,
508
- "avg_time_per_image": 3.0960039062507954
509
  },
510
- "cer_delta": 0.013642277273339637,
511
- "digit_cer_delta": 0.035679012345678995,
512
  "table_score_degradation": 0.0,
513
- "sensitivity_rank": 2
514
  }
515
  }
516
- }
 
1
  {
2
  "baseline": {
3
+ "num_samples": 10,
4
+ "num_digit_samples": 10,
5
  "num_table_samples": 3,
6
+ "mean_cer": 0.20230911688823577,
7
+ "mean_digit_cer": 0.07236995104039168,
8
  "mean_table_score": 1.0,
9
+ "total_inference_time": 126.64344700002403,
10
+ "avg_time_per_image": 12.664344700002403
11
  },
12
  "model_path": "baidu/Unlimited-OCR",
13
+ "source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
14
+ "eval_dir": "calibration",
15
+ "dataset": {
16
+ "name": "calibration",
17
+ "num_samples": 10,
18
+ "num_digit_samples": 10,
19
+ "num_cjk_samples": 3,
20
+ "num_table_samples": 3,
21
+ "category_counts": {
22
+ "cjk_document": 3,
23
+ "english_document": 2,
24
+ "low_quality_scan": 2,
25
+ "table_numeric": 3
26
+ },
27
+ "manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
28
+ "content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
29
+ "files": [
30
+ {
31
+ "path": "images/0001.png",
32
+ "size": 243645,
33
+ "sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
34
+ },
35
+ {
36
+ "path": "ground_truth/0001.txt",
37
+ "size": 1346,
38
+ "sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
39
+ },
40
+ {
41
+ "path": "images/0002.png",
42
+ "size": 208699,
43
+ "sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
44
+ },
45
+ {
46
+ "path": "ground_truth/0002.txt",
47
+ "size": 838,
48
+ "sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
49
+ },
50
+ {
51
+ "path": "images/0003.png",
52
+ "size": 40039,
53
+ "sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
54
+ },
55
+ {
56
+ "path": "ground_truth/0003.txt",
57
+ "size": 459,
58
+ "sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
59
+ },
60
+ {
61
+ "path": "images/0004.png",
62
+ "size": 40356,
63
+ "sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
64
+ },
65
+ {
66
+ "path": "ground_truth/0004.txt",
67
+ "size": 459,
68
+ "sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
69
+ },
70
+ {
71
+ "path": "images/0005.png",
72
+ "size": 40650,
73
+ "sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
74
+ },
75
+ {
76
+ "path": "ground_truth/0005.txt",
77
+ "size": 459,
78
+ "sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
79
+ },
80
+ {
81
+ "path": "images/0006.png",
82
+ "size": 46036,
83
+ "sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
84
+ },
85
+ {
86
+ "path": "ground_truth/0006.txt",
87
+ "size": 191,
88
+ "sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
89
+ },
90
+ {
91
+ "path": "images/0007.png",
92
+ "size": 41828,
93
+ "sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
94
+ },
95
+ {
96
+ "path": "ground_truth/0007.txt",
97
+ "size": 195,
98
+ "sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
99
+ },
100
+ {
101
+ "path": "images/0008.png",
102
+ "size": 20975,
103
+ "sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
104
+ },
105
+ {
106
+ "path": "ground_truth/0008.txt",
107
+ "size": 201,
108
+ "sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
109
+ },
110
+ {
111
+ "path": "images/0009.png",
112
+ "size": 487313,
113
+ "sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
114
+ },
115
+ {
116
+ "path": "ground_truth/0009.txt",
117
+ "size": 121,
118
+ "sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
119
+ },
120
+ {
121
+ "path": "images/0010.png",
122
+ "size": 515820,
123
+ "sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
124
+ },
125
+ {
126
+ "path": "ground_truth/0010.txt",
127
+ "size": 121,
128
+ "sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
129
+ }
130
+ ]
131
+ },
132
  "prompt": "<image>document parsing.",
133
  "max_tokens": 1024,
134
  "groups_tested": [
 
296
  "vision_model.transformer.layers.9.self_attn.qkv_proj"
297
  ],
298
  "metrics": {
299
+ "num_samples": 10,
300
+ "num_digit_samples": 10,
301
  "num_table_samples": 3,
302
+ "mean_cer": 2.4745422809406823,
303
+ "mean_digit_cer": 1.7688242659575963,
304
  "mean_table_score": 0.0,
305
+ "total_inference_time": 310.5234005409875,
306
+ "avg_time_per_image": 31.05234005409875
307
  },
308
+ "cer_delta": 2.2722331640524467,
309
+ "digit_cer_delta": 1.6964543149172047,
310
  "table_score_degradation": 1.0,
311
  "sensitivity_rank": 1
312
  },
 
317
  "projector.layers"
318
  ],
319
  "metrics": {
320
+ "num_samples": 10,
321
+ "num_digit_samples": 10,
322
  "num_table_samples": 3,
323
+ "mean_cer": 0.20560582018493906,
324
+ "mean_digit_cer": 0.07236995104039168,
325
  "mean_table_score": 1.0,
326
+ "total_inference_time": 125.59270616796857,
327
+ "avg_time_per_image": 12.559270616796857
328
  },
329
+ "cer_delta": 0.003296703296703285,
330
+ "digit_cer_delta": 0.0,
331
  "table_score_degradation": 0.0,
332
+ "sensitivity_rank": 3
333
  },
334
  "token_embeddings": {
335
  "status": "success",
 
338
  "language_model.model.embed_tokens"
339
  ],
340
  "metrics": {
341
+ "num_samples": 10,
342
+ "num_digit_samples": 10,
343
  "num_table_samples": 3,
344
+ "mean_cer": 0.19852784584239075,
345
+ "mean_digit_cer": 0.07026468788249693,
346
  "mean_table_score": 1.0,
347
+ "total_inference_time": 125.69877116801217,
348
+ "avg_time_per_image": 12.569877116801218
349
  },
350
+ "cer_delta": -0.003781271045845025,
351
+ "digit_cer_delta": -0.002105263157894749,
352
  "table_score_degradation": 0.0,
353
  "sensitivity_rank": 10
354
  },
 
370
  "language_model.model.layers.9.self_attn.q_proj"
371
  ],
372
  "metrics": {
373
+ "num_samples": 10,
374
+ "num_digit_samples": 10,
375
  "num_table_samples": 3,
376
+ "mean_cer": 0.1810308435920253,
377
+ "mean_digit_cer": 0.08710679314565484,
378
  "mean_table_score": 1.0,
379
+ "total_inference_time": 126.60937520701555,
380
+ "avg_time_per_image": 12.660937520701555
381
  },
382
+ "cer_delta": -0.021278273296210487,
383
+ "digit_cer_delta": 0.01473684210526316,
384
  "table_score_degradation": 0.0,
385
  "sensitivity_rank": 11
386
  },
 
402
  "language_model.model.layers.9.self_attn.k_proj"
403
  ],
404
  "metrics": {
405
+ "num_samples": 10,
406
+ "num_digit_samples": 10,
407
  "num_table_samples": 3,
408
+ "mean_cer": 0.1995659706473317,
409
+ "mean_digit_cer": 0.07236995104039168,
410
  "mean_table_score": 1.0,
411
+ "total_inference_time": 125.29329520699685,
412
+ "avg_time_per_image": 12.529329520699685
413
  },
414
+ "cer_delta": -0.0027431462409040763,
415
+ "digit_cer_delta": 0.0,
416
  "table_score_degradation": 0.0,
417
  "sensitivity_rank": 9
418
  },
 
434
  "language_model.model.layers.9.self_attn.v_proj"
435
  ],
436
  "metrics": {
437
+ "num_samples": 10,
438
+ "num_digit_samples": 10,
439
  "num_table_samples": 3,
440
+ "mean_cer": 0.20121021578933468,
441
+ "mean_digit_cer": 0.07236995104039168,
442
  "mean_table_score": 1.0,
443
+ "total_inference_time": 126.87845074798679,
444
+ "avg_time_per_image": 12.687845074798679
445
  },
446
+ "cer_delta": -0.001098901098901095,
447
+ "digit_cer_delta": 0.0,
448
  "table_score_degradation": 0.0,
449
+ "sensitivity_rank": 6
450
  },
451
  "attention_o_proj": {
452
  "status": "success",
 
466
  "language_model.model.layers.9.self_attn.o_proj"
467
  ],
468
  "metrics": {
469
+ "num_samples": 10,
470
+ "num_digit_samples": 10,
471
  "num_table_samples": 3,
472
+ "mean_cer": 0.2058682899333709,
473
+ "mean_digit_cer": 0.06383873929008568,
474
  "mean_table_score": 1.0,
475
+ "total_inference_time": 128.61252554102975,
476
+ "avg_time_per_image": 12.861252554102975
477
  },
478
+ "cer_delta": 0.0035591730451351167,
479
+ "digit_cer_delta": -0.008531211750305995,
480
  "table_score_degradation": 0.0,
481
+ "sensitivity_rank": 2
482
  },
483
  "dense_mlp": {
484
  "status": "success",
 
489
  "language_model.model.layers.0.mlp.up_proj"
490
  ],
491
  "metrics": {
492
+ "num_samples": 10,
493
+ "num_digit_samples": 10,
494
  "num_table_samples": 3,
495
+ "mean_cer": 0.20121021578933468,
496
+ "mean_digit_cer": 0.07236995104039168,
497
  "mean_table_score": 1.0,
498
+ "total_inference_time": 127.19930095798918,
499
+ "avg_time_per_image": 12.719930095798919
500
  },
501
+ "cer_delta": -0.001098901098901095,
502
+ "digit_cer_delta": 0.0,
503
  "table_score_degradation": 0.0,
504
+ "sensitivity_rank": 7
505
  },
506
  "shared_experts": {
507
  "status": "success",
 
542
  "language_model.model.layers.9.mlp.shared_experts.up_proj"
543
  ],
544
  "metrics": {
545
+ "num_samples": 10,
546
+ "num_digit_samples": 10,
547
  "num_table_samples": 3,
548
+ "mean_cer": 0.20009091014419536,
549
+ "mean_digit_cer": 0.08267594859241126,
550
  "mean_table_score": 1.0,
551
+ "total_inference_time": 128.06347554198874,
552
+ "avg_time_per_image": 12.806347554198874
553
  },
554
+ "cer_delta": -0.002218206744040413,
555
+ "digit_cer_delta": 0.01030599755201958,
556
  "table_score_degradation": 0.0,
557
+ "sensitivity_rank": 8
558
  },
559
  "routed_experts": {
560
  "status": "success",
 
595
  "language_model.model.layers.9.mlp.switch_mlp.up_proj"
596
  ],
597
  "metrics": {
598
+ "num_samples": 10,
599
+ "num_digit_samples": 10,
600
  "num_table_samples": 3,
601
+ "mean_cer": 0.2033268371469385,
602
+ "mean_digit_cer": 0.06899173806609546,
603
  "mean_table_score": 1.0,
604
+ "total_inference_time": 125.24760646000504,
605
+ "avg_time_per_image": 12.524760646000505
606
  },
607
+ "cer_delta": 0.0010177202587027256,
608
+ "digit_cer_delta": -0.0033782129742962186,
609
  "table_score_degradation": 0.0,
610
+ "sensitivity_rank": 5
611
  },
612
  "lm_head": {
613
  "status": "success",
 
616
  "language_model.lm_head"
617
  ],
618
  "metrics": {
619
+ "num_samples": 10,
620
+ "num_digit_samples": 10,
621
  "num_table_samples": 3,
622
+ "mean_cer": 0.20560582018493906,
623
+ "mean_digit_cer": 0.07236995104039168,
624
  "mean_table_score": 1.0,
625
+ "total_inference_time": 122.19146683299914,
626
+ "avg_time_per_image": 12.219146683299915
627
  },
628
+ "cer_delta": 0.003296703296703285,
629
+ "digit_cer_delta": 0.0,
630
  "table_score_degradation": 0.0,
631
+ "sensitivity_rank": 4
632
  }
633
  }
634
+ }
src/unlimited_ocr/__init__.py CHANGED
@@ -1,15 +1,18 @@
1
- """Unlimited-OCR MLX — Improved inference pipeline for Apple Silicon."""
2
 
3
- __version__ = "0.2.0"
4
 
5
  from unlimited_ocr.pipeline import BatchProcessingError, OCRPipeline
6
  from unlimited_ocr.engine import OCREngine
7
  from unlimited_ocr.profiles import get_profile, list_profiles, PROFILES
 
8
 
9
  __all__ = [
10
  "OCRPipeline",
11
  "BatchProcessingError",
12
  "OCREngine",
 
 
13
  "get_profile",
14
  "list_profiles",
15
  "PROFILES",
 
1
+ """Unlimited-OCR inference toolkit for MLX and CUDA/vLLM."""
2
 
3
+ __version__ = "0.3.0"
4
 
5
  from unlimited_ocr.pipeline import BatchProcessingError, OCRPipeline
6
  from unlimited_ocr.engine import OCREngine
7
  from unlimited_ocr.profiles import get_profile, list_profiles, PROFILES
8
+ from unlimited_ocr.vllm_backend import VLLMEngine, VLLMEngineConfig
9
 
10
  __all__ = [
11
  "OCRPipeline",
12
  "BatchProcessingError",
13
  "OCREngine",
14
+ "VLLMEngine",
15
+ "VLLMEngineConfig",
16
  "get_profile",
17
  "list_profiles",
18
  "PROFILES",
src/unlimited_ocr/adapter_registry.json CHANGED
@@ -5,12 +5,20 @@
5
  "table": {
6
  "path": "adapters/table-v1",
7
  "hf_repo": "AutomatosX/unlimited-ocr-lora-table",
8
- "description": "Table structure recognition adapter"
 
 
 
 
9
  },
10
  "receipt": {
11
  "path": "adapters/receipt-v1",
12
  "hf_repo": "AutomatosX/unlimited-ocr-lora-receipt",
13
- "description": "Receipt and invoice parsing adapter"
 
 
 
 
14
  }
15
  },
16
  "default": null
 
5
  "table": {
6
  "path": "adapters/table-v1",
7
  "hf_repo": "AutomatosX/unlimited-ocr-lora-table",
8
+ "description": "Table structure recognition adapter",
9
+ "base_model": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
10
+ "target_scope": "all supported language-model linear modules except lm_head",
11
+ "lora_rank": 16,
12
+ "scene": "table_numeric"
13
  },
14
  "receipt": {
15
  "path": "adapters/receipt-v1",
16
  "hf_repo": "AutomatosX/unlimited-ocr-lora-receipt",
17
+ "description": "Receipt and invoice parsing adapter",
18
+ "base_model": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
19
+ "target_scope": "all supported language-model linear modules except lm_head",
20
+ "lora_rank": 16,
21
+ "scene": "receipt"
22
  }
23
  },
24
  "default": null
src/unlimited_ocr/cli.py CHANGED
@@ -1,4 +1,4 @@
1
- """Command-line interface for Unlimited-OCR MLX."""
2
 
3
  from __future__ import annotations
4
 
@@ -12,6 +12,7 @@ from unlimited_ocr.engine import DEFAULT_MODEL
12
  from unlimited_ocr.pdf import DEFAULT_PDF_DPI
13
  from unlimited_ocr.preprocessing import PREPROCESS_MODES
14
  from unlimited_ocr.profiles import PROFILES
 
15
 
16
  console = Console()
17
  status_console = Console(stderr=True)
@@ -65,7 +66,11 @@ def _resolve_adapter(name_or_path: str) -> str:
65
  from huggingface_hub import snapshot_download
66
  return snapshot_download(
67
  hf_repo,
68
- allow_patterns=["adapter_config.json", "adapters.safetensors"],
 
 
 
 
69
  )
70
  except Exception as exc:
71
  raise click.ClickException(
@@ -84,10 +89,11 @@ def _resolve_adapter(name_or_path: str) -> str:
84
  @click.pass_context
85
  @click.version_option(version=__version__, prog_name="unlimited-ocr")
86
  def main(ctx):
87
- """Unlimited-OCR MLX One-shot document parsing on Apple Silicon.
88
 
89
  Run OCR on images, PDFs, or entire directories using the Unlimited-OCR
90
- vision-language model optimized for MLX (MX FP8 quantized, ~3.6 GB).
 
91
 
92
  \b
93
  Examples:
@@ -123,18 +129,32 @@ def main(ctx):
123
  help="DPI for PDF rendering (150 is usually enough; model input is 1024px).")
124
  @click.option("--max-tokens", type=click.IntRange(min=1), default=None,
125
  help="Maximum tokens per page (overrides profile default).")
126
- @click.option("--model-path", type=str, default=DEFAULT_MODEL,
127
- help="Model path (HF repo ID or local directory).")
 
 
 
 
 
 
 
 
 
 
128
  @click.option("--verbose", is_flag=True, default=False,
129
  help="Enable verbose output.")
130
  @click.option("--stream", is_flag=True, default=False,
131
  help="Stream output token-by-token (single image only).")
132
  @click.option("--jobs", type=click.IntRange(min=1), default=1,
133
  help="Parallel preprocessing workers for batch mode.")
 
 
134
  @click.option("--adapter", type=str, default=None,
135
  help="LoRA adapter name or path (e.g. 'table' or 'adapters/table-v1').")
136
  def run(input_path, output_path, fmt, task, profile, grounding, preprocess,
137
- preprocess_mode, dpi, max_tokens, model_path, verbose, stream, jobs, adapter):
 
 
138
  """Run OCR on an image, PDF, or directory of images.
139
 
140
  INPUT_PATH can be an image file, a PDF file, or a directory of images.
@@ -154,12 +174,30 @@ def run(input_path, output_path, fmt, task, profile, grounding, preprocess,
154
  raise click.UsageError("--stream does not support PDF files")
155
  if jobs != 1:
156
  raise click.UsageError("--jobs only applies to directory batch processing")
157
- elif not input_path.is_dir() and jobs != 1:
158
- raise click.UsageError("--jobs only applies to directory batch processing")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  if verbose:
161
- status_console.print(f"[bold]Unlimited-OCR MLX[/] v{__version__}")
162
- status_console.print(f"[dim]Model:[/] {model_path}")
163
  status_console.print(f"[dim]Input:[/] {input_path}")
164
  status_console.print(f"[dim]Profile:[/] {profile} | Format: {fmt} | Task: {task}")
165
  if preprocess:
@@ -174,17 +212,23 @@ def run(input_path, output_path, fmt, task, profile, grounding, preprocess,
174
  adapter_path = _resolve_adapter(adapter) if adapter else None
175
 
176
  pipeline = OCRPipeline(
177
- model_path=model_path,
 
178
  verbose=verbose,
179
  preprocess=preprocess,
180
  preprocess_mode=preprocess_mode,
181
  profile=profile,
182
  adapter_path=adapter_path,
 
 
 
 
183
  )
184
  if input_path.is_dir():
185
  # Batch mode
186
  _run_batch(pipeline, input_path, output_path, fmt, task,
187
- grounding, max_tokens, profile, jobs)
 
188
  elif stream:
189
  # Streaming mode (single file, stdout only)
190
  _run_stream(pipeline, input_path, task, grounding, dpi, max_tokens, profile)
@@ -242,7 +286,8 @@ def _run_single(pipeline, input_path, output_path, fmt, task, grounding, dpi, ma
242
  click.echo(result)
243
 
244
 
245
- def _run_batch(pipeline, input_dir, output_path, fmt, task, grounding, max_tokens, profile, jobs=1):
 
246
  """Handle batch directory processing."""
247
  results = pipeline.run_batch(
248
  input_dir=input_dir,
@@ -255,6 +300,7 @@ def _run_batch(pipeline, input_dir, output_path, fmt, task, grounding, max_token
255
  profile=profile,
256
  raise_on_error=True,
257
  jobs=jobs,
 
258
  )
259
 
260
  if output_path:
@@ -305,6 +351,9 @@ def models():
305
  console.print(f" [green]•[/] {DEFAULT_MODEL}")
306
  console.print(" MX FP8 quantized (~3.6 GB) — recommended for Apple Silicon")
307
  console.print()
 
 
 
308
  console.print("[dim]Use --model-path to specify a different model.[/]")
309
 
310
 
@@ -322,6 +371,94 @@ def profiles():
322
  console.print()
323
 
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  @main.command()
326
  @click.option("--host", type=str, default="127.0.0.1", show_default=True,
327
  help="Bind host. Use 0.0.0.0 only to expose the API on your network.")
 
1
+ """Command-line interface for Unlimited-OCR MLX and vLLM backends."""
2
 
3
  from __future__ import annotations
4
 
 
12
  from unlimited_ocr.pdf import DEFAULT_PDF_DPI
13
  from unlimited_ocr.preprocessing import PREPROCESS_MODES
14
  from unlimited_ocr.profiles import PROFILES
15
+ from unlimited_ocr.vllm_backend import DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL
16
 
17
  console = Console()
18
  status_console = Console(stderr=True)
 
66
  from huggingface_hub import snapshot_download
67
  return snapshot_download(
68
  hf_repo,
69
+ allow_patterns=[
70
+ "adapter_config.json",
71
+ "adapters.safetensors",
72
+ "training_config.json",
73
+ ],
74
  )
75
  except Exception as exc:
76
  raise click.ClickException(
 
89
  @click.pass_context
90
  @click.version_option(version=__version__, prog_name="unlimited-ocr")
91
  def main(ctx):
92
+ """Unlimited-OCR — document parsing with MLX or CUDA/vLLM.
93
 
94
  Run OCR on images, PDFs, or entire directories using the Unlimited-OCR
95
+ vision-language model through local MLX or an OpenAI-compatible vLLM
96
+ server on NVIDIA CUDA.
97
 
98
  \b
99
  Examples:
 
129
  help="DPI for PDF rendering (150 is usually enough; model input is 1024px).")
130
  @click.option("--max-tokens", type=click.IntRange(min=1), default=None,
131
  help="Maximum tokens per page (overrides profile default).")
132
+ @click.option("--backend", type=click.Choice(["mlx", "vllm"]), default="mlx",
133
+ show_default=True, help="Inference backend.")
134
+ @click.option("--model-path", type=str, default=None,
135
+ help="Model path/served name. Defaults depend on --backend.")
136
+ @click.option("--vllm-base-url", type=str, default=DEFAULT_VLLM_BASE_URL,
137
+ show_default=True, help="OpenAI-compatible vLLM API root.")
138
+ @click.option("--vllm-api-key", type=str, default=None, envvar="VLLM_API_KEY",
139
+ help="vLLM bearer token (or set VLLM_API_KEY).")
140
+ @click.option("--vllm-timeout", type=click.FloatRange(min=0.001), default=3600.0,
141
+ show_default=True, help="vLLM request timeout in seconds.")
142
+ @click.option("--vllm-verify-tls/--no-vllm-verify-tls", default=True,
143
+ help="Verify TLS certificates for the vLLM endpoint.")
144
  @click.option("--verbose", is_flag=True, default=False,
145
  help="Enable verbose output.")
146
  @click.option("--stream", is_flag=True, default=False,
147
  help="Stream output token-by-token (single image only).")
148
  @click.option("--jobs", type=click.IntRange(min=1), default=1,
149
  help="Parallel preprocessing workers for batch mode.")
150
+ @click.option("--request-concurrency", type=click.IntRange(min=1), default=1,
151
+ help="Concurrent vLLM requests for directory batch mode.")
152
  @click.option("--adapter", type=str, default=None,
153
  help="LoRA adapter name or path (e.g. 'table' or 'adapters/table-v1').")
154
  def run(input_path, output_path, fmt, task, profile, grounding, preprocess,
155
+ preprocess_mode, dpi, max_tokens, backend, model_path, vllm_base_url,
156
+ vllm_api_key, vllm_timeout, vllm_verify_tls, verbose, stream, jobs,
157
+ request_concurrency, adapter):
158
  """Run OCR on an image, PDF, or directory of images.
159
 
160
  INPUT_PATH can be an image file, a PDF file, or a directory of images.
 
174
  raise click.UsageError("--stream does not support PDF files")
175
  if jobs != 1:
176
  raise click.UsageError("--jobs only applies to directory batch processing")
177
+ if request_concurrency != 1:
178
+ raise click.UsageError(
179
+ "--request-concurrency only applies to directory batch processing"
180
+ )
181
+ elif not input_path.is_dir() and (jobs != 1 or request_concurrency != 1):
182
+ raise click.UsageError(
183
+ "--jobs and --request-concurrency only apply to directory batch processing"
184
+ )
185
+ if request_concurrency > 1 and backend != "vllm":
186
+ raise click.UsageError(
187
+ "--request-concurrency > 1 requires --backend vllm"
188
+ )
189
+ if backend == "vllm" and adapter:
190
+ raise click.UsageError(
191
+ "--adapter is MLX-only; configure LoRA modules when starting vLLM"
192
+ )
193
+
194
+ resolved_model = model_path or (
195
+ DEFAULT_VLLM_MODEL if backend == "vllm" else DEFAULT_MODEL
196
+ )
197
 
198
  if verbose:
199
+ status_console.print(f"[bold]Unlimited-OCR {backend.upper()}[/] v{__version__}")
200
+ status_console.print(f"[dim]Model:[/] {resolved_model}")
201
  status_console.print(f"[dim]Input:[/] {input_path}")
202
  status_console.print(f"[dim]Profile:[/] {profile} | Format: {fmt} | Task: {task}")
203
  if preprocess:
 
212
  adapter_path = _resolve_adapter(adapter) if adapter else None
213
 
214
  pipeline = OCRPipeline(
215
+ backend=backend,
216
+ model_path=resolved_model,
217
  verbose=verbose,
218
  preprocess=preprocess,
219
  preprocess_mode=preprocess_mode,
220
  profile=profile,
221
  adapter_path=adapter_path,
222
+ vllm_base_url=vllm_base_url,
223
+ vllm_api_key=vllm_api_key,
224
+ vllm_timeout=vllm_timeout,
225
+ vllm_verify_tls=vllm_verify_tls,
226
  )
227
  if input_path.is_dir():
228
  # Batch mode
229
  _run_batch(pipeline, input_path, output_path, fmt, task,
230
+ grounding, max_tokens, profile, jobs,
231
+ request_concurrency)
232
  elif stream:
233
  # Streaming mode (single file, stdout only)
234
  _run_stream(pipeline, input_path, task, grounding, dpi, max_tokens, profile)
 
286
  click.echo(result)
287
 
288
 
289
+ def _run_batch(pipeline, input_dir, output_path, fmt, task, grounding,
290
+ max_tokens, profile, jobs=1, request_concurrency=1):
291
  """Handle batch directory processing."""
292
  results = pipeline.run_batch(
293
  input_dir=input_dir,
 
300
  profile=profile,
301
  raise_on_error=True,
302
  jobs=jobs,
303
+ inference_concurrency=request_concurrency,
304
  )
305
 
306
  if output_path:
 
351
  console.print(f" [green]•[/] {DEFAULT_MODEL}")
352
  console.print(" MX FP8 quantized (~3.6 GB) — recommended for Apple Silicon")
353
  console.print()
354
+ console.print(f" [green]•[/] {DEFAULT_VLLM_MODEL}")
355
+ console.print(" BF16 CUDA/vLLM baseline — use an AWQ repo with --model-path when available")
356
+ console.print()
357
  console.print("[dim]Use --model-path to specify a different model.[/]")
358
 
359
 
 
371
  console.print()
372
 
373
 
374
+ @main.command("vllm-serve")
375
+ @click.option("--model-path", type=str, default=DEFAULT_VLLM_MODEL,
376
+ show_default=True, help="Hugging Face repo ID or local CUDA checkpoint.")
377
+ @click.option("--served-model-name", type=str, default=None,
378
+ help="Model name exposed by the OpenAI API (defaults to model path).")
379
+ @click.option("--host", type=str, default="127.0.0.1", show_default=True,
380
+ help="Bind host; protect externally exposed endpoints with --api-key.")
381
+ @click.option("--port", type=click.IntRange(1, 65535), default=8000,
382
+ show_default=True, help="Bind port.")
383
+ @click.option("--tensor-parallel-size", type=click.IntRange(min=1), default=1,
384
+ show_default=True, help="Number of CUDA GPUs used for tensor parallelism.")
385
+ @click.option("--gpu-memory-utilization", type=click.FloatRange(min=0.01, max=1.0),
386
+ default=0.90, show_default=True, help="Per-GPU memory utilization target.")
387
+ @click.option("--max-model-len", type=click.IntRange(min=1), default=32768,
388
+ show_default=True, help="Maximum context length.")
389
+ @click.option("--max-num-seqs", type=click.IntRange(min=1), default=4,
390
+ show_default=True, help="Maximum concurrent sequences.")
391
+ @click.option("--max-images", type=click.IntRange(min=1), default=40,
392
+ show_default=True, help="Maximum images accepted by one request.")
393
+ @click.option("--dtype", type=click.Choice(["auto", "bfloat16", "float16"]),
394
+ default="bfloat16", show_default=True, help="Model compute dtype.")
395
+ @click.option("--revision", type=str, default=None,
396
+ help="Immutable Hugging Face commit revision (recommended for releases).")
397
+ @click.option("--api-key", type=str, default=None, envvar="VLLM_API_KEY",
398
+ help="Protect the OpenAI API with a bearer token (or VLLM_API_KEY).")
399
+ @click.option("--trust-remote-code/--no-trust-remote-code", default=False,
400
+ help="Allow model repository code (not needed for native Unlimited-OCR).")
401
+ @click.option("--dry-run", is_flag=True, default=False,
402
+ help="Print the redacted vLLM command without starting it.")
403
+ def vllm_serve(model_path, served_model_name, host, port,
404
+ tensor_parallel_size, gpu_memory_utilization, max_model_len,
405
+ max_num_seqs, max_images, dtype, revision, api_key,
406
+ trust_remote_code, dry_run):
407
+ """Start the native CUDA vLLM OpenAI-compatible server.
408
+
409
+ Requires Linux, NVIDIA CUDA, and vLLM 0.25.1+. The command installs the
410
+ Unlimited-OCR R-SWA logits processor and required cache settings.
411
+ """
412
+ import shlex
413
+
414
+ from unlimited_ocr.vllm_serve import (
415
+ VLLMServeConfig,
416
+ build_vllm_command,
417
+ redact_vllm_command,
418
+ run_vllm_server,
419
+ )
420
+
421
+ config = VLLMServeConfig(
422
+ model_path=model_path,
423
+ served_model_name=served_model_name,
424
+ host=host,
425
+ port=port,
426
+ tensor_parallel_size=tensor_parallel_size,
427
+ gpu_memory_utilization=gpu_memory_utilization,
428
+ max_model_len=max_model_len,
429
+ max_num_seqs=max_num_seqs,
430
+ max_images_per_prompt=max_images,
431
+ dtype=dtype,
432
+ revision=revision,
433
+ api_key=api_key,
434
+ trust_remote_code=trust_remote_code,
435
+ )
436
+ try:
437
+ command = build_vllm_command(config)
438
+ except (TypeError, ValueError) as exc:
439
+ raise click.UsageError(str(exc)) from exc
440
+
441
+ if dry_run:
442
+ rendered = shlex.join(redact_vllm_command(command))
443
+ if api_key:
444
+ rendered = f"VLLM_API_KEY=*** {rendered}"
445
+ click.echo(rendered)
446
+ return
447
+
448
+ if host not in {"127.0.0.1", "localhost", "::1"} and not api_key:
449
+ status_console.print(
450
+ "[yellow]Warning:[/] vLLM is exposed without an API key."
451
+ )
452
+ console.print(f"[bold]Starting Unlimited-OCR CUDA/vLLM[/] on {host}:{port}")
453
+ console.print(f"[dim]Model:[/] {model_path}")
454
+ try:
455
+ exit_code = run_vllm_server(config)
456
+ except Exception as exc:
457
+ raise click.ClickException(str(exc)) from exc
458
+ if exit_code != 0:
459
+ raise click.ClickException(f"vLLM exited with status {exit_code}")
460
+
461
+
462
  @main.command()
463
  @click.option("--host", type=str, default="127.0.0.1", show_default=True,
464
  help="Bind host. Use 0.0.0.0 only to expose the API on your network.")
src/unlimited_ocr/engine.py CHANGED
@@ -2,9 +2,10 @@
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import re
6
  import time
7
- import math
8
  from numbers import Real
9
  from dataclasses import dataclass, field
10
  from pathlib import Path
@@ -34,6 +35,90 @@ PROMPTS = {
34
  GROUNDING_PREFIX = "<|grounding|>"
35
 
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  @dataclass
38
  class OCRResult:
39
  """Result from a single OCR inference."""
@@ -132,24 +217,30 @@ def parse_grounding(text: str) -> list[dict]:
132
  "text": "",
133
  }))
134
 
135
- # Current direct form. The first block may omit its opening det token.
 
136
  direct_pattern = re.compile(
137
  rf"(?:^|<\|det\|>)([^\[\n<]*?)\s*"
138
- rf"({coordinate_pattern.pattern})\s*<\|/det\|>"
139
  r"([^\n<]*)",
140
  re.MULTILINE,
141
  )
142
  for match in direct_pattern.finditer(normalized):
143
  if any(start <= match.start() < end for start, end in ref_spans):
144
  continue
145
- coordinate_match = coordinate_pattern.search(match.group(2))
146
- if coordinate_match is None:
147
  continue
148
- parsed.append((match.start(), {
149
- "label": match.group(1).strip(),
150
- "bbox": coordinate_from_match(coordinate_match),
151
- "text": match.group(7).strip(),
152
- }))
 
 
 
 
 
153
 
154
  parsed.sort(key=lambda item: item[0])
155
  return [item for _, item in parsed]
@@ -206,6 +297,9 @@ class OCREngine:
206
  self._model = None
207
  self._processor = None
208
 
 
 
 
209
  @property
210
  def model(self):
211
  """Lazy-load the MLX model."""
@@ -222,8 +316,6 @@ class OCREngine:
222
 
223
  def _load_model(self):
224
  """Load model and processor from mlx-vlm."""
225
- from mlx_vlm import load
226
-
227
  if self.config.verbose:
228
  console.print(f"[bold blue]Loading model:[/] {self.config.model_path}")
229
 
@@ -231,9 +323,15 @@ class OCREngine:
231
  if self.config.adapter_path is not None:
232
  if not isinstance(self.config.adapter_path, str) or not self.config.adapter_path:
233
  raise ValueError("adapter_path must be a non-empty string or None")
 
 
 
 
234
  # mlx-vlm applies adapters to the nested language model correctly;
235
  # mlx-lm's helper alone can target the wrong module tree for VLMs.
236
  load_kwargs["adapter_path"] = self.config.adapter_path
 
 
237
  self._model, self._processor = load(self.config.model_path, **load_kwargs)
238
 
239
  if self.config.verbose:
@@ -252,32 +350,11 @@ class OCREngine:
252
  grounding: If True, prefix with grounding token for bounding boxes.
253
  custom_prompt: Override with a fully custom prompt (must contain <image>).
254
  """
255
- if custom_prompt is not None:
256
- if not isinstance(custom_prompt, str):
257
- raise TypeError("custom_prompt must be a string")
258
- image_count = custom_prompt.count("<image>")
259
- if image_count != 1:
260
- raise ValueError(
261
- "custom_prompt must contain exactly one literal '<image>' token"
262
- )
263
- prompt = custom_prompt
264
- else:
265
- if task not in PROMPTS:
266
- available = ", ".join(PROMPTS)
267
- raise ValueError(f"Unknown task '{task}'. Available: {available}")
268
- prompt = PROMPTS[task]
269
-
270
- wants_grounding = grounding or GROUNDING_PREFIX in prompt
271
- if wants_grounding:
272
- # Upstream DeepSeek/Unlimited-OCR places grounding after the image
273
- # marker: ``<image>\n<|grounding|>task``. Normalize misplaced or
274
- # duplicate copies in custom prompts to that trained format.
275
- prompt = prompt.replace(GROUNDING_PREFIX, "")
276
- before, after = prompt.split("<image>", 1)
277
- after = after.lstrip("\r\n")
278
- prompt = f"{before}<image>\n{GROUNDING_PREFIX}{after}"
279
-
280
- return prompt
281
 
282
  def _validate_generation_settings(
283
  self,
@@ -560,6 +637,11 @@ class OCREngine:
560
  """Check if the model is currently loaded."""
561
  return self._model is not None
562
 
 
 
 
 
 
563
  def unload(self):
564
  """Release model from memory."""
565
  self._model = None
 
2
 
3
  from __future__ import annotations
4
 
5
+ import json
6
+ import math
7
  import re
8
  import time
 
9
  from numbers import Real
10
  from dataclasses import dataclass, field
11
  from pathlib import Path
 
35
  GROUNDING_PREFIX = "<|grounding|>"
36
 
37
 
38
+ def _same_model_reference(recorded: str, requested: str) -> bool:
39
+ if recorded == requested:
40
+ return True
41
+ recorded_path = Path(recorded).expanduser()
42
+ requested_path = Path(requested).expanduser()
43
+ return (
44
+ recorded_path.exists()
45
+ and requested_path.exists()
46
+ and recorded_path.resolve() == requested_path.resolve()
47
+ )
48
+
49
+
50
+ def _validate_adapter_base_model(adapter_path: str, model_path: str) -> None:
51
+ """Fail before MLX load when a local adapter records another base model."""
52
+ adapter_dir = Path(adapter_path).expanduser()
53
+ if not adapter_dir.is_dir():
54
+ return
55
+ config_path = adapter_dir / "training_config.json"
56
+ if not config_path.exists() and not config_path.is_symlink():
57
+ return
58
+ if not config_path.is_file():
59
+ raise ValueError(
60
+ f"Adapter training config must be a readable file: {config_path}"
61
+ )
62
+ try:
63
+ training_config = json.loads(config_path.read_text(encoding="utf-8"))
64
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
65
+ raise ValueError(f"Adapter has invalid training config: {config_path}") from exc
66
+ if not isinstance(training_config, dict):
67
+ raise ValueError(f"Adapter training config must be an object: {config_path}")
68
+ recorded_base_model = training_config.get("base_model")
69
+ if (
70
+ not isinstance(recorded_base_model, str)
71
+ or not recorded_base_model
72
+ or recorded_base_model != recorded_base_model.strip()
73
+ ):
74
+ raise ValueError(
75
+ f"Adapter training config requires a normalized base_model: {config_path}"
76
+ )
77
+ if not _same_model_reference(recorded_base_model, model_path):
78
+ raise ValueError(
79
+ "Adapter base model mismatch: training config records "
80
+ f"{recorded_base_model!r}, but inference requested {model_path!r}"
81
+ )
82
+
83
+
84
+ def build_ocr_prompt(
85
+ task: str = "document",
86
+ grounding: bool = False,
87
+ custom_prompt: Optional[str] = None,
88
+ ) -> str:
89
+ """Build a backend-independent Unlimited-OCR prompt.
90
+
91
+ Both the local MLX engine and the remote vLLM client must use the same
92
+ trained prompt vocabulary. Keeping prompt normalization here prevents the
93
+ two backends from silently producing different requests.
94
+ """
95
+ if custom_prompt is not None:
96
+ if not isinstance(custom_prompt, str):
97
+ raise TypeError("custom_prompt must be a string")
98
+ image_count = custom_prompt.count("<image>")
99
+ if image_count != 1:
100
+ raise ValueError(
101
+ "custom_prompt must contain exactly one literal '<image>' token"
102
+ )
103
+ prompt = custom_prompt
104
+ else:
105
+ if task not in PROMPTS:
106
+ available = ", ".join(PROMPTS)
107
+ raise ValueError(f"Unknown task '{task}'. Available: {available}")
108
+ prompt = PROMPTS[task]
109
+
110
+ wants_grounding = grounding or GROUNDING_PREFIX in prompt
111
+ if wants_grounding:
112
+ # Upstream DeepSeek/Unlimited-OCR places grounding after the image
113
+ # marker: ``<image>\n<|grounding|>task``.
114
+ prompt = prompt.replace(GROUNDING_PREFIX, "")
115
+ before, after = prompt.split("<image>", 1)
116
+ after = after.lstrip("\r\n")
117
+ prompt = f"{before}<image>\n{GROUNDING_PREFIX}{after}"
118
+
119
+ return prompt
120
+
121
+
122
  @dataclass
123
  class OCRResult:
124
  """Result from a single OCR inference."""
 
217
  "text": "",
218
  }))
219
 
220
+ # Current direct form. The first block may omit its opening det token, and
221
+ # a single det span may list several boxes for the same label.
222
  direct_pattern = re.compile(
223
  rf"(?:^|<\|det\|>)([^\[\n<]*?)\s*"
224
+ rf"((?:{coordinate_pattern.pattern}\s*)+)\s*<\|/det\|>"
225
  r"([^\n<]*)",
226
  re.MULTILINE,
227
  )
228
  for match in direct_pattern.finditer(normalized):
229
  if any(start <= match.start() < end for start, end in ref_spans):
230
  continue
231
+ boxes = list(coordinate_pattern.finditer(match.group(2)))
232
+ if not boxes:
233
  continue
234
+ label = match.group(1).strip()
235
+ # group(2) is the coordinate run (nested number groups inside it);
236
+ # the trailing content capture is the last group.
237
+ text = match.group(match.lastindex).strip() if match.lastindex else ""
238
+ for coordinate_match in boxes:
239
+ parsed.append((match.start() + coordinate_match.start(), {
240
+ "label": label,
241
+ "bbox": coordinate_from_match(coordinate_match),
242
+ "text": text,
243
+ }))
244
 
245
  parsed.sort(key=lambda item: item[0])
246
  return [item for _, item in parsed]
 
297
  self._model = None
298
  self._processor = None
299
 
300
+ backend_name = "mlx"
301
+ supports_multi_image = False
302
+
303
  @property
304
  def model(self):
305
  """Lazy-load the MLX model."""
 
316
 
317
  def _load_model(self):
318
  """Load model and processor from mlx-vlm."""
 
 
319
  if self.config.verbose:
320
  console.print(f"[bold blue]Loading model:[/] {self.config.model_path}")
321
 
 
323
  if self.config.adapter_path is not None:
324
  if not isinstance(self.config.adapter_path, str) or not self.config.adapter_path:
325
  raise ValueError("adapter_path must be a non-empty string or None")
326
+ _validate_adapter_base_model(
327
+ self.config.adapter_path,
328
+ self.config.model_path,
329
+ )
330
  # mlx-vlm applies adapters to the nested language model correctly;
331
  # mlx-lm's helper alone can target the wrong module tree for VLMs.
332
  load_kwargs["adapter_path"] = self.config.adapter_path
333
+ from mlx_vlm import load
334
+
335
  self._model, self._processor = load(self.config.model_path, **load_kwargs)
336
 
337
  if self.config.verbose:
 
350
  grounding: If True, prefix with grounding token for bounding boxes.
351
  custom_prompt: Override with a fully custom prompt (must contain <image>).
352
  """
353
+ return build_ocr_prompt(
354
+ task=task,
355
+ grounding=grounding,
356
+ custom_prompt=custom_prompt,
357
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
  def _validate_generation_settings(
360
  self,
 
637
  """Check if the model is currently loaded."""
638
  return self._model is not None
639
 
640
+ def ensure_ready(self) -> None:
641
+ """Load the local model and processor before accepting work."""
642
+ _ = self.model
643
+ _ = self.processor
644
+
645
  def unload(self):
646
  """Release model from memory."""
647
  self._model = None
src/unlimited_ocr/pdf.py CHANGED
@@ -48,47 +48,53 @@ def _pixmap_to_pil(pixmap: fitz.Pixmap) -> Image.Image:
48
 
49
  Pixmaps may be CMYK/gray or carry premultiplied alpha. Reading samples as
50
  raw RGB packing (or dropping alpha without compositing) produces near-black
51
- garbage. Prefer PyMuPDF's own conversion, then composite onto white so
52
- document pages keep a paper background.
 
 
53
  """
54
- if hasattr(pixmap, "pil_image"):
55
- image = pixmap.pil_image()
56
- else:
57
- # Older PyMuPDF fallback: force DeviceRGB and copy sample bytes.
58
  if pixmap.colorspace is None or pixmap.n - pixmap.alpha != 3:
59
  pixmap = fitz.Pixmap(fitz.csRGB, pixmap)
60
- if pixmap.alpha:
61
- # Premultiplied alpha → composite onto white, then drop A.
62
- import array
63
-
64
- samples = array.array("B", pixmap.samples)
65
- width, height = pixmap.width, pixmap.height
66
- stride = pixmap.n
67
- rgb = bytearray(width * height * 3)
68
- for index in range(width * height):
69
- base = index * stride
70
- alpha = samples[base + 3] / 255.0
71
- for channel in range(3):
72
- value = samples[base + channel] + 255.0 * (1.0 - alpha)
73
- rgb[index * 3 + channel] = 0 if value < 0 else (
74
- 255 if value > 255 else int(value)
75
- )
76
- image = Image.frombytes("RGB", (width, height), bytes(rgb))
77
- else:
78
- image = Image.frombytes(
79
- "RGB",
80
- (pixmap.width, pixmap.height),
81
- bytes(pixmap.samples),
82
- )
83
-
84
- if image.mode == "RGB":
85
- # Copy so the image outlives the temporary pixmap.
86
- return image.copy()
87
- if image.mode == "RGBA":
88
- background = Image.new("RGB", image.size, (255, 255, 255))
89
- background.paste(image, mask=image.getchannel("A"))
90
- return background
91
- return image.convert("RGB")
 
 
 
 
 
92
 
93
 
94
  def iter_pdf_pages(
@@ -185,9 +191,10 @@ def pdf_to_images(
185
  for i, page_idx in enumerate(range(start, end + 1)):
186
  page = doc[page_idx]
187
  pixmap = page.get_pixmap(matrix=mat, alpha=False)
188
-
 
189
  out_path = output_dir / f"page_{page_idx + 1:04d}.png"
190
- pixmap.save(str(out_path))
191
  image_paths.append(out_path)
192
 
193
  if progress_callback:
 
48
 
49
  Pixmaps may be CMYK/gray or carry premultiplied alpha. Reading samples as
50
  raw RGB packing (or dropping alpha without compositing) produces near-black
51
+ garbage. PyMuPDF stores alpha-bearing samples premultiplied, so compositing
52
+ must use ``premul + 255 * (1 - a)`` rather than treating channels as straight
53
+ alpha (PIL's ``paste(..., mask=)`` path). Composite onto white so document
54
+ pages keep a paper background.
55
  """
56
+ if pixmap.alpha:
57
+ # Premultiplied alpha → opaque RGB on white. Do not use pil_image()+paste:
58
+ # that path assumes straight alpha and darkens translucent ink.
 
59
  if pixmap.colorspace is None or pixmap.n - pixmap.alpha != 3:
60
  pixmap = fitz.Pixmap(fitz.csRGB, pixmap)
61
+ import array
62
+
63
+ samples = array.array("B", pixmap.samples)
64
+ width, height = pixmap.width, pixmap.height
65
+ stride = pixmap.n
66
+ alpha_index = stride - 1
67
+ rgb = bytearray(width * height * 3)
68
+ for index in range(width * height):
69
+ base = index * stride
70
+ alpha = samples[base + alpha_index] / 255.0
71
+ inv = 1.0 - alpha
72
+ for channel in range(3):
73
+ value = samples[base + channel] + 255.0 * inv
74
+ rgb[index * 3 + channel] = 0 if value < 0 else (
75
+ 255 if value > 255 else int(value)
76
+ )
77
+ return Image.frombytes("RGB", (width, height), bytes(rgb))
78
+
79
+ # Opaque path: prefer pil_image so CMYK/gray conversion stays with PyMuPDF.
80
+ if hasattr(pixmap, "pil_image"):
81
+ image = pixmap.pil_image()
82
+ if image.mode == "RGB":
83
+ # Copy so the image outlives the temporary pixmap.
84
+ return image.copy()
85
+ if image.mode == "RGBA":
86
+ background = Image.new("RGB", image.size, (255, 255, 255))
87
+ background.paste(image, mask=image.getchannel("A"))
88
+ return background
89
+ return image.convert("RGB")
90
+
91
+ if pixmap.colorspace is None or pixmap.n != 3:
92
+ pixmap = fitz.Pixmap(fitz.csRGB, pixmap)
93
+ return Image.frombytes(
94
+ "RGB",
95
+ (pixmap.width, pixmap.height),
96
+ bytes(pixmap.samples),
97
+ )
98
 
99
 
100
  def iter_pdf_pages(
 
191
  for i, page_idx in enumerate(range(start, end + 1)):
192
  page = doc[page_idx]
193
  pixmap = page.get_pixmap(matrix=mat, alpha=False)
194
+ # Route through the same RGB/alpha normalizer as the in-memory
195
+ # path so unusual page colorspaces cannot fail PNG encoding.
196
  out_path = output_dir / f"page_{page_idx + 1:04d}.png"
197
+ _pixmap_to_pil(pixmap).save(str(out_path))
198
  image_paths.append(out_path)
199
 
200
  if progress_callback:
src/unlimited_ocr/pipeline.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
 
5
  import tempfile
6
  from collections import Counter
7
- from concurrent.futures import ThreadPoolExecutor
8
  from dataclasses import replace
9
  from pathlib import Path
10
  from threading import Lock
@@ -19,6 +19,12 @@ from unlimited_ocr.output import DocumentResult, format_result, save_result
19
  from unlimited_ocr.pdf import DEFAULT_PDF_DPI, get_pdf_info, iter_pdf_pages, is_pdf
20
  from unlimited_ocr.preprocessing import PreprocessConfig, preprocess_to_pil, config_for_mode
21
  from unlimited_ocr.profiles import get_profile
 
 
 
 
 
 
22
 
23
  console = Console(stderr=True)
24
 
@@ -52,7 +58,7 @@ class OCRPipeline:
52
 
53
  def __init__(
54
  self,
55
- model_path: str = DEFAULT_MODEL,
56
  max_tokens: Optional[int] = None,
57
  temperature: Optional[float] = None,
58
  verbose: bool = False,
@@ -61,11 +67,18 @@ class OCRPipeline:
61
  preprocess_config: Optional[PreprocessConfig] = None,
62
  profile: str = "accurate",
63
  adapter_path: Optional[str] = None,
 
 
 
 
 
 
64
  ):
65
  """Initialize the OCR pipeline.
66
 
67
  Args:
68
- model_path: HuggingFace model ID or local path.
 
69
  max_tokens: Optional per-page override for the selected profile.
70
  temperature: Optional sampling-temperature override.
71
  verbose: Enable verbose output.
@@ -74,18 +87,51 @@ class OCRPipeline:
74
  preprocess_config: Custom preprocessing configuration (overrides mode).
75
  profile: Generation profile (accurate/fast/long-document/plain-text/markdown).
76
  adapter_path: Optional LoRA adapter path for scene-specific inference.
 
 
 
 
 
 
77
  """
78
  # Initialize first so cleanup remains safe if later validation raises.
79
  self._tmp_dir: Optional[tempfile.TemporaryDirectory] = None
80
  self._tmp_lock = Lock()
81
- self.engine = OCREngine(EngineConfig(
82
- model_path=model_path,
83
- max_tokens=max_tokens,
84
- temperature=temperature,
85
- verbose=verbose,
86
- profile=profile,
87
- adapter_path=adapter_path,
88
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  self.preprocess_enabled = preprocess
90
  self.preprocess_mode = preprocess_mode
91
  self.preprocess_config = preprocess_config or config_for_mode(preprocess_mode)
@@ -269,6 +315,7 @@ class OCRPipeline:
269
  profile: Optional[str] = None,
270
  raise_on_error: bool = False,
271
  jobs: int = 1,
 
272
  memory_reset_interval: int = 50,
273
  ) -> dict[str, str]:
274
  """Run OCR on all images in a directory.
@@ -286,6 +333,8 @@ class OCRPipeline:
286
  raise_on_error: Raise BatchProcessingError after processing if any
287
  files failed. Successful results remain attached to the error.
288
  jobs: Number of parallel workers for image preprocessing (1=sequential).
 
 
289
  memory_reset_interval: Reset the reported Metal peak-memory
290
  counter every N successful images (0=never).
291
 
@@ -298,6 +347,16 @@ class OCRPipeline:
298
  self._validate_run_options(task, format, profile, max_tokens)
299
  if not isinstance(jobs, int) or isinstance(jobs, bool) or jobs < 1:
300
  raise ValueError("jobs must be a positive integer")
 
 
 
 
 
 
 
 
 
 
301
  if (
302
  not isinstance(memory_reset_interval, int)
303
  or isinstance(memory_reset_interval, bool)
@@ -356,6 +415,7 @@ class OCRPipeline:
356
  preparation_errors[img_file.name] = exc
357
 
358
  processed_count = 0
 
359
 
360
  def _process_one(img_file: Path) -> tuple[str, str]:
361
  """Process a single image, returns (filename, output_text)."""
@@ -387,14 +447,47 @@ class OCRPipeline:
387
  out_path = output_path_for(img_file)
388
  save_result(doc_result, out_path, format)
389
 
390
- processed_count += 1
 
 
 
 
 
391
  # Start a fresh peak-memory reporting window. This does not free
392
  # model memory; MLX manages allocations itself.
393
- if memory_reset_interval > 0 and processed_count % memory_reset_interval == 0:
394
  self._reset_memory_stats()
395
 
396
  return img_file.name, output
397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  if show_progress:
399
  with Progress(
400
  SpinnerColumn(),
@@ -404,20 +497,9 @@ class OCRPipeline:
404
  console=console,
405
  ) as progress:
406
  task_id = progress.add_task("Processing images...", total=len(image_files))
407
- for img_file in image_files:
408
- try:
409
- name, output = _process_one(img_file)
410
- results[name] = output
411
- except Exception as e:
412
- errors.append((img_file.name, str(e)))
413
- progress.advance(task_id)
414
  else:
415
- for img_file in image_files:
416
- try:
417
- name, output = _process_one(img_file)
418
- results[name] = output
419
- except Exception as e:
420
- errors.append((img_file.name, str(e)))
421
 
422
  # Report errors
423
  if errors:
@@ -427,10 +509,18 @@ class OCRPipeline:
427
  if raise_on_error:
428
  raise BatchProcessingError(errors, results)
429
 
430
- return results
 
 
 
 
 
 
431
 
432
  def _reset_memory_stats(self):
433
  """Reset MLX's peak-memory statistic for subsequent measurements."""
 
 
434
  # Avoid importing/initializing MLX merely to reset a statistic when no
435
  # model has been loaded (important on non-Metal and headless hosts).
436
  if not self.engine.is_loaded():
@@ -529,7 +619,7 @@ class OCRPipeline:
529
  )
530
 
531
  def cleanup(self):
532
- """Clean up temporary files."""
533
  lock = getattr(self, "_tmp_lock", None)
534
  if lock is None:
535
  return
@@ -537,6 +627,10 @@ class OCRPipeline:
537
  if getattr(self, "_tmp_dir", None) is not None:
538
  self._tmp_dir.cleanup()
539
  self._tmp_dir = None
 
 
 
 
540
 
541
  def _validate_run_options(
542
  self,
 
4
 
5
  import tempfile
6
  from collections import Counter
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
  from dataclasses import replace
9
  from pathlib import Path
10
  from threading import Lock
 
19
  from unlimited_ocr.pdf import DEFAULT_PDF_DPI, get_pdf_info, iter_pdf_pages, is_pdf
20
  from unlimited_ocr.preprocessing import PreprocessConfig, preprocess_to_pil, config_for_mode
21
  from unlimited_ocr.profiles import get_profile
22
+ from unlimited_ocr.vllm_backend import (
23
+ DEFAULT_VLLM_BASE_URL,
24
+ DEFAULT_VLLM_MODEL,
25
+ VLLMEngine,
26
+ VLLMEngineConfig,
27
+ )
28
 
29
  console = Console(stderr=True)
30
 
 
58
 
59
  def __init__(
60
  self,
61
+ model_path: Optional[str] = None,
62
  max_tokens: Optional[int] = None,
63
  temperature: Optional[float] = None,
64
  verbose: bool = False,
 
67
  preprocess_config: Optional[PreprocessConfig] = None,
68
  profile: str = "accurate",
69
  adapter_path: Optional[str] = None,
70
+ backend: str = "mlx",
71
+ vllm_base_url: str = DEFAULT_VLLM_BASE_URL,
72
+ vllm_api_key: Optional[str] = None,
73
+ vllm_timeout: float = 3600.0,
74
+ vllm_verify_tls: bool = True,
75
+ vllm_max_images: int = 40,
76
  ):
77
  """Initialize the OCR pipeline.
78
 
79
  Args:
80
+ model_path: HuggingFace model ID or local path. Defaults to the
81
+ AutomatosX MLX model for ``mlx`` and upstream BF16 for ``vllm``.
82
  max_tokens: Optional per-page override for the selected profile.
83
  temperature: Optional sampling-temperature override.
84
  verbose: Enable verbose output.
 
87
  preprocess_config: Custom preprocessing configuration (overrides mode).
88
  profile: Generation profile (accurate/fast/long-document/plain-text/markdown).
89
  adapter_path: Optional LoRA adapter path for scene-specific inference.
90
+ backend: Inference backend (``mlx`` or ``vllm``).
91
+ vllm_base_url: OpenAI-compatible vLLM API root.
92
+ vllm_api_key: Optional vLLM bearer token.
93
+ vllm_timeout: Remote inference timeout in seconds.
94
+ vllm_verify_tls: Verify HTTPS certificates for vLLM requests.
95
+ vllm_max_images: Maximum images in one vLLM request.
96
  """
97
  # Initialize first so cleanup remains safe if later validation raises.
98
  self._tmp_dir: Optional[tempfile.TemporaryDirectory] = None
99
  self._tmp_lock = Lock()
100
+ if not isinstance(backend, str):
101
+ raise TypeError("backend must be a string")
102
+ backend_name = backend.strip().lower()
103
+ if backend_name not in {"mlx", "vllm"}:
104
+ raise ValueError("backend must be one of: mlx, vllm")
105
+ self.backend = backend_name
106
+ if backend_name == "mlx":
107
+ resolved_model = model_path or DEFAULT_MODEL
108
+ self.engine = OCREngine(EngineConfig(
109
+ model_path=resolved_model,
110
+ max_tokens=max_tokens,
111
+ temperature=temperature,
112
+ verbose=verbose,
113
+ profile=profile,
114
+ adapter_path=adapter_path,
115
+ ))
116
+ else:
117
+ if adapter_path is not None:
118
+ raise ValueError(
119
+ "adapter_path is not supported by the vLLM client; "
120
+ "configure LoRA modules when starting vLLM"
121
+ )
122
+ resolved_model = model_path or DEFAULT_VLLM_MODEL
123
+ self.engine = VLLMEngine(VLLMEngineConfig(
124
+ model_path=resolved_model,
125
+ base_url=vllm_base_url,
126
+ api_key=vllm_api_key,
127
+ timeout_seconds=vllm_timeout,
128
+ verify_tls=vllm_verify_tls,
129
+ max_tokens=max_tokens,
130
+ temperature=temperature,
131
+ verbose=verbose,
132
+ profile=profile,
133
+ max_images_per_request=vllm_max_images,
134
+ ))
135
  self.preprocess_enabled = preprocess
136
  self.preprocess_mode = preprocess_mode
137
  self.preprocess_config = preprocess_config or config_for_mode(preprocess_mode)
 
315
  profile: Optional[str] = None,
316
  raise_on_error: bool = False,
317
  jobs: int = 1,
318
+ inference_concurrency: int = 1,
319
  memory_reset_interval: int = 50,
320
  ) -> dict[str, str]:
321
  """Run OCR on all images in a directory.
 
333
  raise_on_error: Raise BatchProcessingError after processing if any
334
  files failed. Successful results remain attached to the error.
335
  jobs: Number of parallel workers for image preprocessing (1=sequential).
336
+ inference_concurrency: Concurrent remote inference requests. Values
337
+ above 1 are supported only by the vLLM backend.
338
  memory_reset_interval: Reset the reported Metal peak-memory
339
  counter every N successful images (0=never).
340
 
 
347
  self._validate_run_options(task, format, profile, max_tokens)
348
  if not isinstance(jobs, int) or isinstance(jobs, bool) or jobs < 1:
349
  raise ValueError("jobs must be a positive integer")
350
+ if (
351
+ not isinstance(inference_concurrency, int)
352
+ or isinstance(inference_concurrency, bool)
353
+ or inference_concurrency < 1
354
+ ):
355
+ raise ValueError("inference_concurrency must be a positive integer")
356
+ if inference_concurrency > 1 and self.backend != "vllm":
357
+ raise ValueError(
358
+ "inference_concurrency > 1 is supported only by the vLLM backend"
359
+ )
360
  if (
361
  not isinstance(memory_reset_interval, int)
362
  or isinstance(memory_reset_interval, bool)
 
415
  preparation_errors[img_file.name] = exc
416
 
417
  processed_count = 0
418
+ processed_count_lock = Lock()
419
 
420
  def _process_one(img_file: Path) -> tuple[str, str]:
421
  """Process a single image, returns (filename, output_text)."""
 
447
  out_path = output_path_for(img_file)
448
  save_result(doc_result, out_path, format)
449
 
450
+ with processed_count_lock:
451
+ processed_count += 1
452
+ reset_memory = (
453
+ memory_reset_interval > 0
454
+ and processed_count % memory_reset_interval == 0
455
+ )
456
  # Start a fresh peak-memory reporting window. This does not free
457
  # model memory; MLX manages allocations itself.
458
+ if reset_memory:
459
  self._reset_memory_stats()
460
 
461
  return img_file.name, output
462
 
463
+ def _run_all(advance: Callable[[], None]) -> None:
464
+ if inference_concurrency > 1:
465
+ # Resolve readiness once before worker threads issue requests.
466
+ self.engine.ensure_ready()
467
+ with ThreadPoolExecutor(
468
+ max_workers=inference_concurrency
469
+ ) as executor:
470
+ futures = {
471
+ executor.submit(_process_one, img_file): img_file
472
+ for img_file in image_files
473
+ }
474
+ for future in as_completed(futures):
475
+ img_file = futures[future]
476
+ try:
477
+ name, output = future.result()
478
+ results[name] = output
479
+ except Exception as exc:
480
+ errors.append((img_file.name, str(exc)))
481
+ advance()
482
+ return
483
+ for img_file in image_files:
484
+ try:
485
+ name, output = _process_one(img_file)
486
+ results[name] = output
487
+ except Exception as exc:
488
+ errors.append((img_file.name, str(exc)))
489
+ advance()
490
+
491
  if show_progress:
492
  with Progress(
493
  SpinnerColumn(),
 
497
  console=console,
498
  ) as progress:
499
  task_id = progress.add_task("Processing images...", total=len(image_files))
500
+ _run_all(lambda: progress.advance(task_id))
 
 
 
 
 
 
501
  else:
502
+ _run_all(lambda: None)
 
 
 
 
 
503
 
504
  # Report errors
505
  if errors:
 
509
  if raise_on_error:
510
  raise BatchProcessingError(errors, results)
511
 
512
+ # Preserve deterministic input order even when remote requests finish
513
+ # out of order.
514
+ return {
515
+ image_file.name: results[image_file.name]
516
+ for image_file in image_files
517
+ if image_file.name in results
518
+ }
519
 
520
  def _reset_memory_stats(self):
521
  """Reset MLX's peak-memory statistic for subsequent measurements."""
522
+ if getattr(self.engine, "backend_name", "mlx") != "mlx":
523
+ return
524
  # Avoid importing/initializing MLX merely to reset a statistic when no
525
  # model has been loaded (important on non-Metal and headless hosts).
526
  if not self.engine.is_loaded():
 
619
  )
620
 
621
  def cleanup(self):
622
+ """Clean up temporary files and release the selected backend."""
623
  lock = getattr(self, "_tmp_lock", None)
624
  if lock is None:
625
  return
 
627
  if getattr(self, "_tmp_dir", None) is not None:
628
  self._tmp_dir.cleanup()
629
  self._tmp_dir = None
630
+ engine = getattr(self, "engine", None)
631
+ unload = getattr(engine, "unload", None)
632
+ if callable(unload):
633
+ unload()
634
 
635
  def _validate_run_options(
636
  self,
src/unlimited_ocr/preprocessing.py CHANGED
@@ -544,6 +544,11 @@ def apply_gamma(img: np.ndarray, gamma: float = 1.5) -> np.ndarray:
544
  raise ValueError("gamma must be a positive finite number")
545
  # Quantize the cache key so near-identical values share a LUT.
546
  cache_key = round(float(gamma), 4)
 
 
 
 
 
547
  table = _GAMMA_LUT_CACHE.get(cache_key)
548
  if table is None:
549
  inv_gamma = 1.0 / cache_key
@@ -680,6 +685,11 @@ def preprocess_image(
680
  img, rot_angle = auto_rotate(img)
681
  if rot_angle:
682
  config.applied_steps.append(f"rotate_{rot_angle}")
 
 
 
 
 
683
  img = sauvola_binarize(img)
684
  config.applied_steps.append("sauvola_binarize")
685
  if config.target_size:
 
544
  raise ValueError("gamma must be a positive finite number")
545
  # Quantize the cache key so near-identical values share a LUT.
546
  cache_key = round(float(gamma), 4)
547
+ # Very small positive values round to zero at the cache precision. Keep
548
+ # the exact value in that case so valid input cannot become a division by
549
+ # zero merely because of the LUT cache key.
550
+ if cache_key == 0:
551
+ cache_key = float(gamma)
552
  table = _GAMMA_LUT_CACHE.get(cache_key)
553
  if table is None:
554
  inv_gamma = 1.0 / cache_key
 
685
  img, rot_angle = auto_rotate(img)
686
  if rot_angle:
687
  config.applied_steps.append(f"rotate_{rot_angle}")
688
+ if deskew:
689
+ deskewed = deskew_image(img)
690
+ if deskewed is not img:
691
+ config.applied_steps.append("deskew")
692
+ img = deskewed
693
  img = sauvola_binarize(img)
694
  config.applied_steps.append("sauvola_binarize")
695
  if config.target_size:
tokenizer_config.json CHANGED
@@ -13,7 +13,7 @@
13
  "local_files_only": false,
14
  "model_max_length": 1000000000000000019884624838656,
15
  "pad_token": "<|▁pad▁|>",
16
- "processor_class": "UnlimitedOCRProcessor",
17
  "tokenizer_class": "TokenizersBackend",
18
  "unk_token": null,
19
  "use_default_system_prompt": false
 
13
  "local_files_only": false,
14
  "model_max_length": 1000000000000000019884624838656,
15
  "pad_token": "<|▁pad▁|>",
16
+ "processor_class": "UnlimitedOCRHFProcessor",
17
  "tokenizer_class": "TokenizersBackend",
18
  "unk_token": null,
19
  "use_default_system_prompt": false