AutomatosX commited on
Commit
fd0c04f
·
verified ·
1 Parent(s): 02b21ca

Publish verified OCR-aware MXFP8 checkpoint

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. LICENSE +21 -0
  2. README.md +84 -0
  3. benchmarks/datasets.md +57 -0
  4. benchmarks/evaluate_cer.py +279 -0
  5. benchmarks/evaluate_tables.py +285 -0
  6. benchmarks/normalize_output.py +128 -0
  7. benchmarks/rswa_validation.py +500 -0
  8. benchmarks/run_accuracy.py +249 -0
  9. benchmarks/run_performance.py +280 -0
  10. chat_template.jinja +1 -0
  11. config.json +1321 -0
  12. docs/PRD.md +153 -0
  13. docs/TECHNICAL_SPEC.md +260 -0
  14. docs/adr/0001-verified-ocr-aware-quantization.md +90 -0
  15. docs/adr/0002-calibrated-affine8-lm-head.md +60 -0
  16. examples/batch_directory.py +52 -0
  17. examples/multi_page_pdf.py +53 -0
  18. examples/single_image.py +36 -0
  19. generation_config.json +17 -0
  20. model.safetensors +3 -0
  21. model.safetensors.index.json +759 -0
  22. precision_map.json +110 -0
  23. preprocessing_config.json +81 -0
  24. processor_config.json +28 -0
  25. pyproject.toml +53 -0
  26. quantization/calibrate_precision.py +142 -0
  27. quantization/layer_sensitivity.py +402 -0
  28. quantization/mixed_precision_convert.py +323 -0
  29. quantization/precision_map.json +31 -0
  30. quantization/release_gate.py +584 -0
  31. quantization/run_pipeline.py +340 -0
  32. quantization_config.json +26 -0
  33. quantization_summary.json +257 -0
  34. release/bf16_accuracy.json +195 -0
  35. release/calibration_results.json +92 -0
  36. release/candidate_accuracy.json +195 -0
  37. release/candidate_performance.json +68 -0
  38. release/candidate_rswa.json +75 -0
  39. release/generated_precision_map.json +110 -0
  40. release/provenance.json +174 -0
  41. release/reference_accuracy.json +195 -0
  42. release/reference_performance.json +68 -0
  43. release/release_manifest.json +1191 -0
  44. release/sensitivity_results.json +516 -0
  45. special_tokens_map.json +39 -0
  46. src/unlimited_ocr/__init__.py +17 -0
  47. src/unlimited_ocr/adapter_registry.json +17 -0
  48. src/unlimited_ocr/cli.py +368 -0
  49. src/unlimited_ocr/engine.py +566 -0
  50. src/unlimited_ocr/output.py +492 -0
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AutomatosX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ base_model: baidu/Unlimited-OCR
4
+ library_name: mlx-vlm
5
+ pipeline_tag: image-text-to-text
6
+ tags:
7
+ - mlx
8
+ - mlx-vlm
9
+ - ocr
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
19
+ created from the upstream BF16 weights. The
20
+ [`sahilchachra/unlimited-ocr-mxfp8-mlx`](https://huggingface.co/sahilchachra/unlimited-ocr-mxfp8-mlx)
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
27
+ 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
69
+ `docs/`.
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 \
78
+ --prompt '<image>document parsing.' \
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.
benchmarks/datasets.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Benchmark Datasets
2
+
3
+ ## Evaluation Dataset Requirements
4
+
5
+ The evaluation set must be separate from any calibration data used for quantization decisions.
6
+
7
+ ### Recommended Composition (minimum 500 samples)
8
+
9
+ | Category | Minimum Samples |
10
+ |---------------------------|----------------:|
11
+ | English printed documents | 75 |
12
+ | Simplified Chinese | 50 |
13
+ | Traditional Chinese | 50 |
14
+ | Chinese-English mixed | 50 |
15
+ | Tables | 75 |
16
+ | Forms | 50 |
17
+ | Invoices and receipts | 50 |
18
+ | Mathematical formulas | 30 |
19
+ | Multicolumn documents | 40 |
20
+ | Low-quality scans | 50 |
21
+ | Long text pages | 30 |
22
+
23
+ ### Suggested Public Sources
24
+
25
+ - **PubTabNet** — table structure recognition
26
+ - **CORD** — consolidated receipt dataset
27
+ - **SROIE** — scanned receipts OCR
28
+ - **ICDAR 2019** — document images
29
+ - **CASIA** — Chinese handwritten/printed
30
+ - **Custom Traditional Chinese** — government forms, academic papers
31
+
32
+ ### Directory Structure
33
+
34
+ ```
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.
44
+ For table evaluation, include the expected markdown table structure.
45
+
46
+ ### Evaluation Protocol
47
+
48
+ 1. All models compared using identical:
49
+ - Input images (no preprocessing unless explicitly testing preprocessing)
50
+ - Prompt: `<image>document parsing.`
51
+ - Max tokens: 8192
52
+ - Temperature: 0.0 (deterministic)
53
+ - Output normalization (via `normalize_output.py`)
54
+
55
+ 2. Report per-category results, not just aggregate.
56
+
57
+ 3. Separate raw-input and preprocessed-input results.
benchmarks/evaluate_cer.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Character and Word Error Rate evaluation for OCR output.
2
+
3
+ Provides CER, WER, digit-specific CER, and normalized edit distance.
4
+
5
+ Usage:
6
+ from benchmarks.evaluate_cer import compute_cer, compute_digit_cer, evaluate_file
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import unicodedata
13
+ from pathlib import Path
14
+
15
+ import numpy as np
16
+
17
+
18
+ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".bmp"}
19
+
20
+
21
+ def extract_cjk_characters(text: str) -> list[str]:
22
+ """Return Chinese, Japanese, and Korean script characters from text."""
23
+ ranges = (
24
+ (0x3400, 0x4DBF), # CJK Extension A
25
+ (0x4E00, 0x9FFF), # CJK Unified Ideographs
26
+ (0xF900, 0xFAFF), # CJK Compatibility Ideographs
27
+ (0x20000, 0x2FA1F), # CJK supplementary extensions
28
+ (0x30000, 0x323AF), # CJK Extensions G and H
29
+ (0x3040, 0x309F), # Hiragana
30
+ (0x30A0, 0x30FF), # Katakana
31
+ (0x31F0, 0x31FF), # Katakana phonetic extensions
32
+ (0xFF66, 0xFF9D), # Half-width Katakana
33
+ (0x1B000, 0x1B16F), # Kana supplements and small kana
34
+ (0x1100, 0x11FF), # Hangul Jamo
35
+ (0x3130, 0x318F), # Hangul compatibility Jamo
36
+ (0xA960, 0xA97F), # Hangul Jamo Extended-A
37
+ (0xAC00, 0xD7AF), # Hangul syllables
38
+ (0xD7B0, 0xD7FF), # Hangul Jamo Extended-B
39
+ )
40
+ return [
41
+ char
42
+ for char in text
43
+ if any(start <= ord(char) <= end for start, end in ranges)
44
+ ]
45
+
46
+
47
+ def normalize_text(text: str) -> str:
48
+ """Normalize text for fair comparison.
49
+
50
+ - Unicode NFC normalization
51
+ - Collapse whitespace
52
+ - Strip leading/trailing whitespace
53
+ """
54
+ text = unicodedata.normalize("NFC", text)
55
+ text = re.sub(r"\s+", " ", text)
56
+ return text.strip()
57
+
58
+
59
+ def edit_distance(ref: list, hyp: list) -> int:
60
+ """Compute Levenshtein edit distance between two sequences."""
61
+ n, m = len(ref), len(hyp)
62
+ if n == 0:
63
+ return m
64
+ if m == 0:
65
+ return n
66
+
67
+ # Use two rows for memory efficiency
68
+ prev = list(range(m + 1))
69
+ curr = [0] * (m + 1)
70
+
71
+ for i in range(1, n + 1):
72
+ curr[0] = i
73
+ for j in range(1, m + 1):
74
+ if ref[i - 1] == hyp[j - 1]:
75
+ curr[j] = prev[j - 1]
76
+ else:
77
+ curr[j] = 1 + min(prev[j], curr[j - 1], prev[j - 1])
78
+ prev, curr = curr, prev
79
+
80
+ return prev[m]
81
+
82
+
83
+ def compute_cer(reference: str, hypothesis: str, normalize: bool = True) -> float:
84
+ """Compute Character Error Rate.
85
+
86
+ CER = edit_distance(ref_chars, hyp_chars) / len(ref_chars)
87
+ """
88
+ if normalize:
89
+ reference = normalize_text(reference)
90
+ hypothesis = normalize_text(hypothesis)
91
+
92
+ # Remove spaces for character-level comparison
93
+ ref_chars = list(reference.replace(" ", ""))
94
+ hyp_chars = list(hypothesis.replace(" ", ""))
95
+
96
+ if len(ref_chars) == 0:
97
+ return 0.0 if len(hyp_chars) == 0 else 1.0
98
+
99
+ dist = edit_distance(ref_chars, hyp_chars)
100
+ return dist / len(ref_chars)
101
+
102
+
103
+ def compute_wer(reference: str, hypothesis: str, normalize: bool = True) -> float:
104
+ """Compute Word Error Rate.
105
+
106
+ WER = edit_distance(ref_words, hyp_words) / len(ref_words)
107
+ """
108
+ if normalize:
109
+ reference = normalize_text(reference)
110
+ hypothesis = normalize_text(hypothesis)
111
+
112
+ ref_words = reference.split()
113
+ hyp_words = hypothesis.split()
114
+
115
+ if len(ref_words) == 0:
116
+ return 0.0 if len(hyp_words) == 0 else 1.0
117
+
118
+ dist = edit_distance(ref_words, hyp_words)
119
+ return dist / len(ref_words)
120
+
121
+
122
+ def compute_digit_cer(reference: str, hypothesis: str) -> float:
123
+ """Compute CER only on digit characters (0-9).
124
+
125
+ Critical for financial documents, invoices, and forms.
126
+ """
127
+ ref_digits = re.sub(r"[^0-9]", "", reference)
128
+ hyp_digits = re.sub(r"[^0-9]", "", hypothesis)
129
+
130
+ if len(ref_digits) == 0:
131
+ return 0.0 if len(hyp_digits) == 0 else 1.0
132
+
133
+ dist = edit_distance(list(ref_digits), list(hyp_digits))
134
+ return dist / len(ref_digits)
135
+
136
+
137
+ def compute_cjk_cer(reference: str, hypothesis: str) -> float:
138
+ """Compute CER specifically for CJK characters."""
139
+ ref_cjk = extract_cjk_characters(reference)
140
+ hyp_cjk = extract_cjk_characters(hypothesis)
141
+
142
+ if len(ref_cjk) == 0:
143
+ return 0.0 if len(hyp_cjk) == 0 else 1.0
144
+
145
+ dist = edit_distance(ref_cjk, hyp_cjk)
146
+ return dist / len(ref_cjk)
147
+
148
+
149
+ def detect_repetition(text: str, ngram_size: int = 35, threshold: int = 3) -> float:
150
+ """Detect repetition rate in generated text.
151
+
152
+ Returns the fraction of n-grams that appear more than `threshold` times.
153
+ """
154
+ if not isinstance(ngram_size, int) or isinstance(ngram_size, bool) or ngram_size < 1:
155
+ raise ValueError("ngram_size must be a positive integer")
156
+ if not isinstance(threshold, int) or isinstance(threshold, bool) or threshold < 0:
157
+ raise ValueError("threshold must be a non-negative integer")
158
+
159
+ words = text.split()
160
+ if len(words) < ngram_size:
161
+ return 0.0
162
+
163
+ ngrams = []
164
+ for i in range(len(words) - ngram_size + 1):
165
+ ngrams.append(tuple(words[i:i + ngram_size]))
166
+
167
+ if not ngrams:
168
+ return 0.0
169
+
170
+ from collections import Counter
171
+ counts = Counter(ngrams)
172
+ repeated = sum(1 for c in counts.values() if c > threshold)
173
+ return repeated / len(counts)
174
+
175
+
176
+ def evaluate_text(reference: str, hypothesis: str) -> dict:
177
+ """Evaluate a reference/hypothesis text pair."""
178
+ return {
179
+ "cer": compute_cer(reference, hypothesis),
180
+ "wer": compute_wer(reference, hypothesis),
181
+ "digit_cer": compute_digit_cer(reference, hypothesis),
182
+ "cjk_cer": compute_cjk_cer(reference, hypothesis),
183
+ "repetition_rate": detect_repetition(hypothesis),
184
+ "ref_length": len(reference),
185
+ "hyp_length": len(hypothesis),
186
+ "ref_digit_count": sum(char.isascii() and char.isdigit() for char in reference),
187
+ "ref_cjk_count": len(extract_cjk_characters(reference)),
188
+ }
189
+
190
+
191
+ def evaluate_file(reference_path: str | Path, hypothesis_path: str | Path) -> dict:
192
+ """Evaluate a single reference/hypothesis pair."""
193
+ reference = Path(reference_path).read_text(encoding="utf-8")
194
+ hypothesis = Path(hypothesis_path).read_text(encoding="utf-8")
195
+ return evaluate_text(reference, hypothesis)
196
+
197
+
198
+ def evaluate_directory(
199
+ images_dir: str | Path,
200
+ ground_truth_dir: str | Path,
201
+ hypothesis_dir: str | Path,
202
+ ) -> dict:
203
+ """Evaluate all files in a directory.
204
+
205
+ Returns aggregate metrics.
206
+ """
207
+ image_dir = Path(images_dir)
208
+ gt_dir = Path(ground_truth_dir)
209
+ hyp_dir = Path(hypothesis_dir)
210
+
211
+ for label, directory in (
212
+ ("images", image_dir),
213
+ ("ground truth", gt_dir),
214
+ ("hypothesis", hyp_dir),
215
+ ):
216
+ if not directory.is_dir():
217
+ raise NotADirectoryError(f"{label.title()} directory not found: {directory}")
218
+
219
+ image_files = sorted(
220
+ path
221
+ for path in image_dir.iterdir()
222
+ if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS
223
+ )
224
+ image_stems = [path.stem for path in image_files]
225
+ duplicate_stems = sorted({stem for stem in image_stems if image_stems.count(stem) > 1})
226
+ if duplicate_stems:
227
+ raise ValueError(
228
+ "Multiple input images share the same stem: " + ", ".join(duplicate_stems)
229
+ )
230
+
231
+ results = []
232
+ missing_ground_truth = []
233
+ missing_hypotheses = []
234
+ for image_file in image_files:
235
+ gt_file = gt_dir / f"{image_file.stem}.txt"
236
+ if not gt_file.is_file():
237
+ missing_ground_truth.append(image_file.name)
238
+ continue
239
+ hyp_file = hyp_dir / gt_file.name
240
+ reference = gt_file.read_text(encoding="utf-8")
241
+ if hyp_file.is_file():
242
+ hypothesis = hyp_file.read_text(encoding="utf-8")
243
+ else:
244
+ # A missing prediction is an empty prediction, not a sample that
245
+ # can be silently removed from the accuracy denominator.
246
+ hypothesis = ""
247
+ missing_hypotheses.append(image_file.name)
248
+ result = evaluate_text(reference, hypothesis)
249
+ result["file"] = gt_file.stem
250
+ result["image"] = image_file.name
251
+ result["missing_hypothesis"] = not hyp_file.is_file()
252
+ results.append(result)
253
+
254
+ if not results:
255
+ return {
256
+ "error": "No images with matching ground-truth files found",
257
+ "num_images": len(image_files),
258
+ "num_samples": 0,
259
+ "missing_ground_truth": missing_ground_truth,
260
+ "missing_hypotheses": missing_hypotheses,
261
+ }
262
+
263
+ digit_results = [r["digit_cer"] for r in results if r["ref_digit_count"] > 0]
264
+ cjk_results = [r["cjk_cer"] for r in results if r["ref_cjk_count"] > 0]
265
+
266
+ return {
267
+ "num_images": len(image_files),
268
+ "num_samples": len(results),
269
+ "num_digit_samples": len(digit_results),
270
+ "num_cjk_samples": len(cjk_results),
271
+ "missing_ground_truth": missing_ground_truth,
272
+ "missing_hypotheses": missing_hypotheses,
273
+ "mean_cer": float(np.mean([r["cer"] for r in results])),
274
+ "mean_wer": float(np.mean([r["wer"] for r in results])),
275
+ "mean_digit_cer": float(np.mean(digit_results)) if digit_results else None,
276
+ "mean_cjk_cer": float(np.mean(cjk_results)) if cjk_results else None,
277
+ "mean_repetition_rate": float(np.mean([r["repetition_rate"] for r in results])),
278
+ "per_file": results,
279
+ }
benchmarks/evaluate_tables.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Table structure evaluation (TEDS-like scoring).
2
+
3
+ Provides a simplified Tree-Edit-Distance-based Similarity score for tables
4
+ by comparing HTML table structures.
5
+
6
+ Usage:
7
+ from benchmarks.evaluate_tables import compute_table_score, evaluate_tables
8
+ """
9
+
10
+ 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]:
18
+ """Split a Markdown row, honoring optional outer and escaped pipes."""
19
+ stripped = line.strip()
20
+ if stripped.startswith("|"):
21
+ stripped = stripped[1:]
22
+ if stripped.endswith("|") and not stripped.endswith(r"\|"):
23
+ stripped = stripped[:-1]
24
+ return [
25
+ cell.replace(r"\|", "|").strip()
26
+ for cell in re.split(r"(?<!\\)\|", stripped)
27
+ ]
28
+
29
+
30
+ def _is_separator_row(line: str) -> bool:
31
+ cells = _split_markdown_row(line)
32
+ return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells)
33
+
34
+
35
+ def _extract_markdown_tables_with_positions(text: str) -> list[tuple[int, str]]:
36
+ """Extract Markdown tables together with their character offsets."""
37
+ lines = text.split("\n")
38
+ offsets = []
39
+ offset = 0
40
+ for line in lines:
41
+ offsets.append(offset)
42
+ offset += len(line) + 1
43
+
44
+ tables: list[tuple[int, str]] = []
45
+ index = 0
46
+ while index + 1 < len(lines):
47
+ table_start = index
48
+ header = lines[index].strip()
49
+ separator = lines[index + 1].strip()
50
+ if "|" not in header or not _is_separator_row(separator):
51
+ index += 1
52
+ continue
53
+
54
+ header_cells = _split_markdown_row(header)
55
+ separator_cells = _split_markdown_row(separator)
56
+ if len(header_cells) != len(separator_cells):
57
+ index += 1
58
+ continue
59
+
60
+ current_table = [header, separator]
61
+ index += 2
62
+ while index < len(lines):
63
+ row = lines[index].strip()
64
+ if not row or "|" not in row:
65
+ break
66
+ current_table.append(row)
67
+ index += 1
68
+ tables.append((offsets[table_start], "\n".join(current_table)))
69
+
70
+ return tables
71
+
72
+
73
+ def extract_tables_from_markdown(text: str) -> list[str]:
74
+ """Extract markdown tables from OCR output."""
75
+ return [table for _, table in _extract_markdown_tables_with_positions(text)]
76
+
77
+
78
+ def extract_tables_from_html(text: str) -> list[str]:
79
+ """Extract HTML tables from OCR output."""
80
+ return [table for _, table in _extract_html_tables_with_positions(text)]
81
+
82
+
83
+ def _extract_html_tables_with_positions(text: str) -> list[tuple[int, str]]:
84
+ """Extract balanced outer HTML tables, preserving nested table markup."""
85
+ tag_pattern = re.compile(r"</?table\b[^>]*>", re.IGNORECASE)
86
+ tables: list[tuple[int, str]] = []
87
+ depth = 0
88
+ start: int | None = None
89
+ for match in tag_pattern.finditer(text):
90
+ if re.match(r"<\s*/", match.group(0)):
91
+ if depth == 0:
92
+ continue
93
+ depth -= 1
94
+ if depth == 0 and start is not None:
95
+ tables.append((start, text[start:match.end()]))
96
+ start = None
97
+ else:
98
+ if depth == 0:
99
+ start = match.start()
100
+ depth += 1
101
+ return tables
102
+
103
+
104
+ def _extract_tables_in_document_order(text: str) -> list[str]:
105
+ """Extract Markdown and HTML tables without reordering their occurrences."""
106
+ html_tables = _extract_html_tables_with_positions(text)
107
+ html_ranges = [(start, start + len(table)) for start, table in html_tables]
108
+ located = [
109
+ (start, table)
110
+ for start, table in _extract_markdown_tables_with_positions(text)
111
+ if not any(begin <= start < end for begin, end in html_ranges)
112
+ ]
113
+ located.extend(html_tables)
114
+ located.sort(key=lambda item: item[0])
115
+ return [table for _, table in located]
116
+
117
+
118
+ class _HTMLTableParser(HTMLParser):
119
+ """Collect rows and cells from one HTML table without external parsers."""
120
+
121
+ def __init__(self):
122
+ super().__init__(convert_charrefs=True)
123
+ self.table_depth = 0
124
+ self.rows: list[list[str]] = []
125
+ self._row: list[str] | None = None
126
+ self._cell_parts: list[str] | None = None
127
+
128
+ def handle_starttag(self, tag: str, attrs):
129
+ tag = tag.lower()
130
+ if tag == "table":
131
+ self.table_depth += 1
132
+ elif self.table_depth == 1 and tag == "tr":
133
+ self._row = []
134
+ elif self.table_depth == 1 and tag in {"td", "th"} and self._row is not None:
135
+ self._cell_parts = []
136
+ elif self.table_depth == 1 and tag == "br" and self._cell_parts is not None:
137
+ self._cell_parts.append("\n")
138
+
139
+ def handle_data(self, data: str):
140
+ if self.table_depth == 1 and self._cell_parts is not None:
141
+ self._cell_parts.append(data)
142
+
143
+ def handle_endtag(self, tag: str):
144
+ tag = tag.lower()
145
+ if self.table_depth == 1 and tag in {"td", "th"}:
146
+ if self._row is not None and self._cell_parts is not None:
147
+ value = re.sub(r"\s+", " ", "".join(self._cell_parts)).strip()
148
+ self._row.append(value)
149
+ self._cell_parts = None
150
+ elif self.table_depth == 1 and tag == "tr":
151
+ if self._row is not None and self._row:
152
+ self.rows.append(self._row)
153
+ self._row = None
154
+ self._cell_parts = None
155
+ elif tag == "table" and self.table_depth:
156
+ self.table_depth -= 1
157
+
158
+
159
+ def _normalize_html_table(table_text: str) -> dict:
160
+ parser = _HTMLTableParser()
161
+ parser.feed(table_text)
162
+ parser.close()
163
+ rows = parser.rows
164
+ if not rows:
165
+ return {"num_rows": 0, "num_cols": 0, "cells": []}
166
+ return {
167
+ "num_rows": len(rows),
168
+ "num_cols": max(len(row) for row in rows),
169
+ "cells": [cell for row in rows for cell in row],
170
+ }
171
+
172
+
173
+ def normalize_table(table_text: str) -> dict:
174
+ """Parse a markdown table into a normalized structure.
175
+
176
+ Returns dict with: num_rows, num_cols, cells (flattened list).
177
+ """
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)]
185
+
186
+ rows = []
187
+ for line in data_lines:
188
+ rows.append(_split_markdown_row(line))
189
+
190
+ if not rows:
191
+ return {"num_rows": 0, "num_cols": 0, "cells": []}
192
+
193
+ num_cols = max(len(r) for r in rows) if rows else 0
194
+ cells = []
195
+ for row in rows:
196
+ cells.extend(row)
197
+
198
+ return {
199
+ "num_rows": len(rows),
200
+ "num_cols": num_cols,
201
+ "cells": cells,
202
+ }
203
+
204
+
205
+ def compute_table_score(reference_table: str, hypothesis_table: str) -> float:
206
+ """Compute a simplified TEDS-like score between two tables.
207
+
208
+ Score is based on:
209
+ - Structure match (rows x cols): 40% weight
210
+ - Cell content match: 60% weight
211
+
212
+ Returns score in [0, 1] where 1 = perfect match.
213
+ """
214
+ ref = normalize_table(reference_table)
215
+ hyp = normalize_table(hypothesis_table)
216
+
217
+ if ref["num_rows"] == 0 and hyp["num_rows"] == 0:
218
+ return 1.0
219
+ if ref["num_rows"] == 0 or hyp["num_rows"] == 0:
220
+ return 0.0
221
+
222
+ # Structure score
223
+ row_match = 1.0 - abs(ref["num_rows"] - hyp["num_rows"]) / max(ref["num_rows"], hyp["num_rows"])
224
+ col_match = 1.0 - abs(ref["num_cols"] - hyp["num_cols"]) / max(ref["num_cols"], hyp["num_cols"])
225
+ structure_score = (row_match + col_match) / 2
226
+
227
+ # Cell content score (compare flattened cells)
228
+ ref_cells = ref["cells"]
229
+ hyp_cells = hyp["cells"]
230
+
231
+ if not ref_cells:
232
+ return structure_score * 0.4
233
+
234
+ # Simple cell-by-cell comparison
235
+ max_len = max(len(ref_cells), len(hyp_cells))
236
+ matches = 0
237
+ for i in range(min(len(ref_cells), len(hyp_cells))):
238
+ # Normalize whitespace for comparison
239
+ rc = re.sub(r"\s+", " ", ref_cells[i]).strip().lower()
240
+ hc = re.sub(r"\s+", " ", hyp_cells[i]).strip().lower()
241
+ if rc == hc:
242
+ matches += 1
243
+ elif rc and hc and (rc in hc or hc in rc):
244
+ matches += 0.5
245
+
246
+ cell_score = matches / max_len if max_len > 0 else 0.0
247
+
248
+ # Weighted combination
249
+ return 0.4 * structure_score + 0.6 * cell_score
250
+
251
+
252
+ def evaluate_tables(reference_text: str, hypothesis_text: str) -> dict:
253
+ """Evaluate table extraction quality between reference and hypothesis.
254
+
255
+ Returns dict with: num_ref_tables, num_hyp_tables, mean_score, per_table_scores.
256
+ """
257
+ ref_tables = _extract_tables_in_document_order(reference_text)
258
+ hyp_tables = _extract_tables_in_document_order(hypothesis_text)
259
+
260
+ if not ref_tables:
261
+ return {
262
+ "num_ref_tables": 0,
263
+ "num_hyp_tables": len(hyp_tables),
264
+ "mean_score": 1.0 if not hyp_tables else 0.0,
265
+ "per_table_scores": [0.0] * len(hyp_tables),
266
+ }
267
+
268
+ # Match tables by position (simplified)
269
+ scores = []
270
+ for i, ref_table in enumerate(ref_tables):
271
+ if i < len(hyp_tables):
272
+ score = compute_table_score(ref_table, hyp_tables[i])
273
+ else:
274
+ score = 0.0 # Missing table
275
+ scores.append(score)
276
+
277
+ # Hallucinated extra tables must count against the result as well.
278
+ scores.extend([0.0] * max(0, len(hyp_tables) - len(ref_tables)))
279
+
280
+ return {
281
+ "num_ref_tables": len(ref_tables),
282
+ "num_hyp_tables": len(hyp_tables),
283
+ "mean_score": float(sum(scores) / len(scores)) if scores else 0.0,
284
+ "per_table_scores": scores,
285
+ }
benchmarks/normalize_output.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Normalize OCR output for fair comparison.
2
+
3
+ Strips model-specific artifacts, normalizes whitespace, and removes
4
+ special tokens before evaluation.
5
+
6
+ Usage:
7
+ python benchmarks/normalize_output.py input.txt output.txt
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import re
14
+ import unicodedata
15
+ from pathlib import Path
16
+
17
+
18
+ def normalize_ocr_output(text: str) -> str:
19
+ """Normalize OCR output for evaluation.
20
+
21
+ Steps:
22
+ 1. Remove special tokens (<|det|>, <|/det|>, <|grounding|>, etc.)
23
+ 2. Unicode NFC normalization
24
+ 3. Normalize whitespace (collapse multiple spaces/newlines)
25
+ 4. Strip leading/trailing whitespace
26
+ 5. Normalize common OCR artifacts
27
+ """
28
+ if not isinstance(text, str):
29
+ raise TypeError("text must be a string")
30
+
31
+ # Tokenizer-visible whitespace markers occasionally survive decoding.
32
+ text = text.replace("Ġ", " ").replace("Ċ", "\n")
33
+
34
+ # Remove grounding coordinates only when they are part of a detection
35
+ # span. A blanket ``[digits]`` removal corrupts legitimate text such as
36
+ # citations and list markers.
37
+ number = r"-?\d+(?:\.\d+)?"
38
+ coordinate_pattern = re.compile(
39
+ rf"\[\s*{number}\s*,\s*{number}\s*,\s*{number}\s*,\s*{number}\s*\]"
40
+ )
41
+
42
+ # In the older ref/det form, the ref span is recognized content and should
43
+ # be retained while the following coordinate-only det span is removed.
44
+ text = re.sub(
45
+ r"<\|ref\|>(.*?)<\|/ref\|>\s*<\|det\|>.*?<\|/det\|>",
46
+ lambda match: match.group(1),
47
+ text,
48
+ flags=re.DOTALL,
49
+ )
50
+
51
+ def clean_detection(match: re.Match) -> str:
52
+ inner = match.group(1)
53
+ # Current output uses ``<|det|>layout-label [box]<|/det|>content``.
54
+ # Both the layout label and coordinates are metadata, not OCR text.
55
+ return "" if coordinate_pattern.search(inner) else inner
56
+
57
+ text = re.sub(
58
+ r"<\|det\|>(.*?)<\|/det\|>",
59
+ clean_detection,
60
+ text,
61
+ flags=re.DOTALL,
62
+ )
63
+ # Some decoded outputs omit the first opening <|det|> token.
64
+ text = re.sub(
65
+ rf"(?m)(^|\n)[^\n<]*?{coordinate_pattern.pattern}"
66
+ rf"(?=\s*<\|/det\|>)",
67
+ lambda match: match.group(1),
68
+ text,
69
+ )
70
+
71
+ # Remove special tokens
72
+ text = re.sub(r"<\|[^|]*\|>", "", text)
73
+ text = re.sub(r"<|[^|]*|>", "", text)
74
+ text = re.sub(r"<PAGE>", "\n", text, flags=re.IGNORECASE)
75
+ text = re.sub(r"<image>", "", text)
76
+
77
+ # Unicode normalization
78
+ text = unicodedata.normalize("NFC", text)
79
+
80
+ # Normalize different types of whitespace
81
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
82
+
83
+ # Collapse multiple blank lines to max 2
84
+ text = re.sub(r"\n{3,}", "\n\n", text)
85
+
86
+ # Collapse multiple spaces (but preserve single newlines)
87
+ text = re.sub(r"[^\S\n]+", " ", text)
88
+
89
+ # Strip each line
90
+ lines = [line.strip() for line in text.split("\n")]
91
+ text = "\n".join(lines)
92
+
93
+ # Whitespace-only lines become empty only after the per-line strip above,
94
+ # so enforce the blank-line limit again afterwards.
95
+ text = re.sub(r"\n{3,}", "\n\n", text)
96
+
97
+ # Strip overall
98
+ text = text.strip()
99
+
100
+ return text
101
+
102
+
103
+ def normalize_for_digit_comparison(text: str) -> str:
104
+ """Extract only digits and decimal points for numeric comparison."""
105
+ return re.sub(r"[^0-9.]", "", text)
106
+
107
+
108
+ def main():
109
+ parser = argparse.ArgumentParser(description="Normalize OCR output for evaluation")
110
+ parser.add_argument("input", type=Path, help="Raw OCR text file")
111
+ parser.add_argument("output", type=Path, help="Normalized output file")
112
+ args = parser.parse_args()
113
+
114
+ input_path = args.input
115
+ output_path = args.output
116
+
117
+ text = input_path.read_text(encoding="utf-8")
118
+ normalized = normalize_ocr_output(text)
119
+ output_path.parent.mkdir(parents=True, exist_ok=True)
120
+ output_path.write_text(normalized, encoding="utf-8")
121
+
122
+ print(f"Normalized: {input_path} -> {output_path}")
123
+ print(f" Input: {len(text)} chars")
124
+ print(f" Output: {len(normalized)} chars")
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
benchmarks/rswa_validation.py ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """R-SWA (Reference Sliding Window Attention) cache validation.
2
+
3
+ Instruments MLX KV-cache behavior at increasing output lengths to verify
4
+ whether the model's R-SWA mechanism properly bounds cache growth.
5
+
6
+ Usage:
7
+ python benchmarks/rswa_validation.py \
8
+ --model-path AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
9
+ --image test_image.jpg \
10
+ --output benchmarks/results/rswa_results.json
11
+ """
12
+
13
+ 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]
27
+
28
+
29
+ class MinGeneratedTokensProcessor:
30
+ """Suppress EOS until a requested number of new tokens has been sampled.
31
+
32
+ ``mlx-vlm`` passes prompt tokens to logits processors as well as generated
33
+ tokens. The first observed token count is therefore recorded as the prompt
34
+ length, keeping this stress-test control independent of prompt tokenization.
35
+ A fresh processor must be constructed for every generation call.
36
+ """
37
+
38
+ def __init__(self, min_new_tokens: int, eos_token_ids: list[int]):
39
+ if (
40
+ not isinstance(min_new_tokens, int)
41
+ or isinstance(min_new_tokens, bool)
42
+ or min_new_tokens < 1
43
+ ):
44
+ raise ValueError("min_new_tokens must be a positive integer")
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")
52
+ self.min_new_tokens = min_new_tokens
53
+ self.eos_token_ids = normalized_ids
54
+ self._prompt_token_count: int | None = None
55
+
56
+ def __call__(self, tokens, logits):
57
+ sequence = tokens.tolist() if hasattr(tokens, "tolist") else list(tokens)
58
+ if sequence and isinstance(sequence[0], list):
59
+ sequence = sequence[0]
60
+ if self._prompt_token_count is None:
61
+ self._prompt_token_count = len(sequence)
62
+ generated_tokens = max(0, len(sequence) - self._prompt_token_count)
63
+ if generated_tokens < self.min_new_tokens:
64
+ logits[:, self.eos_token_ids] = float("-inf")
65
+ return logits
66
+
67
+
68
+ def collect_eos_token_ids(model, processor) -> list[int]:
69
+ """Collect EOS IDs from model config and the loaded tokenizer wrapper."""
70
+ values = []
71
+ model_config = getattr(model, "config", None)
72
+ values.append(getattr(model_config, "eos_token_id", None))
73
+
74
+ tokenizer = getattr(processor, "tokenizer", processor)
75
+ values.append(getattr(tokenizer, "eos_token_ids", None))
76
+ values.append(getattr(tokenizer, "eos_token_id", None))
77
+ stopping_criteria = getattr(tokenizer, "stopping_criteria", None)
78
+ values.append(getattr(stopping_criteria, "eos_token_ids", None))
79
+
80
+ normalized: set[int] = set()
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)
89
+
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
100
+
101
+ from collections import Counter
102
+ ngrams = [tuple(words[i:i+ngram_size]) for i in range(len(words) - ngram_size + 1)]
103
+ counts = Counter(ngrams)
104
+ repeated = sum(1 for c in counts.values() if c > threshold)
105
+ return repeated / len(counts) if counts else 0.0
106
+
107
+
108
+ def analyze_cache_behavior(results: list[dict]) -> tuple[bool | None, str]:
109
+ """Conservatively classify cache growth from successful length-limited runs.
110
+
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:
131
+ return None, "Need at least three distinct generated lengths"
132
+ if points[-1][0] < points[0][0] * 2:
133
+ return None, "Generated token counts span less than 2x"
134
+
135
+ # Peak-memory readings can fluctuate slightly, so enforce their cumulative
136
+ # maximum before comparing early and late slopes.
137
+ monotonic_memory = []
138
+ running_peak = 0.0
139
+ for tokens, memory in points:
140
+ running_peak = max(running_peak, float(memory))
141
+ monotonic_memory.append((tokens, running_peak))
142
+
143
+ midpoint = len(monotonic_memory) // 2
144
+ first_tokens, first_memory = monotonic_memory[0]
145
+ middle_tokens, middle_memory = monotonic_memory[midpoint]
146
+ last_tokens, last_memory = monotonic_memory[-1]
147
+ early_span = middle_tokens - first_tokens
148
+ late_span = last_tokens - middle_tokens
149
+ if early_span <= 0 or late_span <= 0:
150
+ return None, "Generated lengths are not sufficiently distinct"
151
+
152
+ total_memory_change = last_memory - first_memory
153
+ # A wide token span with <=64 MB of additional peak memory is direct
154
+ # plateau evidence even if the early slope is effectively zero.
155
+ if last_tokens >= first_tokens * 4 and total_memory_change <= 64.0:
156
+ return True, "Peak memory stayed within 64 MB across a >=4x token span"
157
+
158
+ early_slope = (middle_memory - first_memory) / early_span
159
+ late_slope = (last_memory - middle_memory) / late_span
160
+ if early_slope <= 0:
161
+ return None, "Memory variation is too small to estimate a growth trend"
162
+
163
+ if late_slope <= early_slope * 0.25:
164
+ return True, "Late memory-growth slope is <=25% of the early slope"
165
+ return False, "Memory growth does not plateau at longer generated lengths"
166
+
167
+
168
+ def run_rswa_validation(
169
+ model_path: str,
170
+ image_path: str,
171
+ prompt: str = "<image>document parsing.",
172
+ test_lengths: list[int] | None = None,
173
+ force_min_tokens: int | None = None,
174
+ ) -> dict:
175
+ """Run R-SWA cache validation at increasing output lengths.
176
+
177
+ Measures:
178
+ - Peak memory at each output length
179
+ - Decode TPS in first vs last quarter
180
+ - Cache growth behavior
181
+ - Repetition rate
182
+ - Completion status
183
+ """
184
+ if test_lengths is None:
185
+ test_lengths = list(TEST_LENGTHS)
186
+ else:
187
+ test_lengths = list(test_lengths)
188
+ if not Path(image_path).is_file():
189
+ raise FileNotFoundError(f"Test image not found: {image_path}")
190
+ if not isinstance(prompt, str) or prompt.count("<image>") != 1:
191
+ raise ValueError("prompt must contain exactly one literal '<image>' token")
192
+ if not test_lengths or any(
193
+ not isinstance(length, int) or isinstance(length, bool) or length < 1
194
+ for length in test_lengths
195
+ ):
196
+ raise ValueError("test_lengths must contain positive integers")
197
+ if len(set(test_lengths)) != len(test_lengths):
198
+ raise ValueError("test_lengths must not contain duplicates")
199
+ if test_lengths != sorted(test_lengths):
200
+ raise ValueError("test_lengths must be in increasing order")
201
+ if len(test_lengths) < 3:
202
+ raise ValueError("test_lengths must contain at least three lengths")
203
+ if 8192 not in test_lengths:
204
+ raise ValueError("test_lengths must include 8192 for the required 8K check")
205
+ if test_lengths[-1] < test_lengths[0] * 2:
206
+ raise ValueError("test_lengths must span at least 2x")
207
+ if force_min_tokens is not None and (
208
+ not isinstance(force_min_tokens, int)
209
+ or isinstance(force_min_tokens, bool)
210
+ or force_min_tokens < 1
211
+ ):
212
+ raise ValueError("force_min_tokens must be a positive integer or None")
213
+
214
+ from mlx_vlm import load, generate
215
+ import mlx.core as mx
216
+
217
+ print(f"Loading model: {model_path}")
218
+ model, processor = load(model_path)
219
+ eos_token_ids = (
220
+ collect_eos_token_ids(model, processor)
221
+ if force_min_tokens is not None
222
+ else []
223
+ )
224
+
225
+ baseline_memory = mx.get_active_memory() / 1e6
226
+ print(f"Baseline memory: {baseline_memory:.0f} MB")
227
+
228
+ results = []
229
+
230
+ for max_tokens in test_lengths:
231
+ print(f"\n--- Testing max_tokens={max_tokens} ---")
232
+
233
+ mx.clear_cache()
234
+ mem_before = mx.get_active_memory() / 1e6
235
+ mx.reset_peak_memory()
236
+ start = time.perf_counter()
237
+
238
+ try:
239
+ forced_minimum = (
240
+ min(force_min_tokens, max_tokens)
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,
252
+ image=[image_path],
253
+ max_tokens=max_tokens,
254
+ temperature=0.0,
255
+ logits_processors=logits_processors,
256
+ verbose=False,
257
+ )
258
+ elapsed = time.perf_counter() - start
259
+ peak_memory = (
260
+ response.get("peak_memory")
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):
272
+ text = response
273
+ tokens_generated = len(text.split())
274
+ token_count_source = "whitespace estimate"
275
+ tokens_reliable = False
276
+ elif hasattr(response, "text"):
277
+ text = response.text
278
+ if not isinstance(text, str):
279
+ raise TypeError("mlx-vlm returned a non-string text field")
280
+ raw_tokens = getattr(
281
+ response,
282
+ "generation_tokens",
283
+ getattr(response, "tokens_generated", None),
284
+ )
285
+ if raw_tokens is None:
286
+ tokens_generated = len(text.split())
287
+ token_count_source = "whitespace estimate"
288
+ tokens_reliable = False
289
+ else:
290
+ tokens_generated = raw_tokens
291
+ token_count_source = "mlx-vlm token count"
292
+ tokens_reliable = True
293
+ elif isinstance(response, dict):
294
+ text = response.get("text", "")
295
+ if not isinstance(text, str):
296
+ raise TypeError("mlx-vlm returned a non-string text field")
297
+ raw_tokens = response.get(
298
+ "generation_tokens", response.get("tokens_generated")
299
+ )
300
+ if raw_tokens is None:
301
+ tokens_generated = len(text.split())
302
+ token_count_source = "whitespace estimate"
303
+ tokens_reliable = False
304
+ else:
305
+ tokens_generated = raw_tokens
306
+ token_count_source = "mlx-vlm token count"
307
+ tokens_reliable = True
308
+ else:
309
+ text = str(response)
310
+ tokens_generated = len(text.split())
311
+ token_count_source = "whitespace estimate"
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)
326
+ finish_reason = (
327
+ response.get("finish_reason")
328
+ if isinstance(response, dict)
329
+ else getattr(response, "finish_reason", None)
330
+ )
331
+ if finish_reason is not None:
332
+ completed = finish_reason != "length"
333
+ elif tokens_reliable:
334
+ completed = tokens_generated < max_tokens
335
+ else:
336
+ completed = None
337
+
338
+ result = {
339
+ "max_tokens": max_tokens,
340
+ "tokens_generated": tokens_generated,
341
+ "tokens_generated_source": token_count_source,
342
+ "tokens_generated_reliable": tokens_reliable,
343
+ "elapsed_seconds": elapsed,
344
+ "mean_tps": tps,
345
+ "tps_source": (
346
+ "mlx-vlm generation_tps" if reported_tps else "end-to-end estimate"
347
+ ),
348
+ "peak_memory_mb": mem_after,
349
+ "memory_growth_mb": memory_growth,
350
+ "repetition_rate": repetition,
351
+ "completed_naturally": completed,
352
+ "finish_reason": finish_reason,
353
+ "forced_minimum_tokens": forced_minimum,
354
+ "status": "success",
355
+ }
356
+
357
+ print(f" Tokens: {tokens_generated} | TPS: {tps:.1f} | "
358
+ f"Memory: {mem_after:.0f} MB (+{memory_growth:.0f}) | "
359
+ f"Repetition: {repetition:.4f}")
360
+
361
+ except Exception as e:
362
+ elapsed = time.perf_counter() - start
363
+ result = {
364
+ "max_tokens": max_tokens,
365
+ "status": "error",
366
+ "error": str(e),
367
+ "elapsed_seconds": elapsed,
368
+ }
369
+ print(f" ERROR: {e}")
370
+
371
+ results.append(result)
372
+
373
+ # Analyze cache behavior
374
+ successful = [r for r in results if r["status"] == "success"]
375
+ reliable_successful = [
376
+ r for r in successful if r.get("tokens_generated_reliable", True)
377
+ ]
378
+ cache_bounded, cache_reason = analyze_cache_behavior(results)
379
+
380
+ # TPS degradation check
381
+ tps_stable: bool | None = None
382
+ distinct_tps_runs = sorted(
383
+ reliable_successful,
384
+ key=lambda result: result.get("tokens_generated", 0),
385
+ )
386
+ if (
387
+ len({r.get("tokens_generated", 0) for r in distinct_tps_runs}) >= 2
388
+ and distinct_tps_runs[-1].get("tokens_generated", 0)
389
+ >= distinct_tps_runs[0].get("tokens_generated", 0) * 2
390
+ ):
391
+ first_tps = distinct_tps_runs[0].get("mean_tps", 0)
392
+ last_tps = distinct_tps_runs[-1].get("mean_tps", 0)
393
+ if first_tps > 0:
394
+ tps_stable = last_tps >= first_tps * 0.7 # Within 30% degradation
395
+
396
+ test_8k = next((r for r in results if r.get("max_tokens") == 8192), None)
397
+ test_8k_passed = bool(
398
+ test_8k
399
+ and test_8k.get("status") == "success"
400
+ and test_8k.get("tokens_generated_reliable", True)
401
+ and test_8k.get("tokens_generated", 0) >= int(8192 * 0.95)
402
+ )
403
+
404
+ summary = {
405
+ "model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
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(),
412
+ "baseline_memory_mb": baseline_memory,
413
+ "test_results": results,
414
+ "analysis": {
415
+ "cache_appears_bounded": cache_bounded,
416
+ "cache_analysis_reason": cache_reason,
417
+ "tps_stable_over_length": tps_stable,
418
+ "max_successful_tokens": max(
419
+ (r["tokens_generated"] for r in reliable_successful), default=0
420
+ ),
421
+ "max_repetition_rate": max(
422
+ (r.get("repetition_rate", 0) for r in successful), default=0
423
+ ),
424
+ },
425
+ "pass_conditions": {
426
+ "cache_bounded": cache_bounded is True,
427
+ "tps_stable": tps_stable is True,
428
+ "8k_test_passed": test_8k_passed,
429
+ },
430
+ }
431
+ summary["passed"] = all(summary["pass_conditions"].values())
432
+
433
+ return summary
434
+
435
+
436
+ def main():
437
+ parser = argparse.ArgumentParser(description="R-SWA cache validation")
438
+ parser.add_argument("--model-path", required=True)
439
+ parser.add_argument("--image", required=True, help="Test image (should produce long output)")
440
+ parser.add_argument("--output", type=Path, default=Path("benchmarks/results/rswa_results.json"))
441
+ parser.add_argument("--prompt", default="<image>document parsing.")
442
+ parser.add_argument("--lengths", type=int, nargs="+", default=None,
443
+ help="Custom output lengths to test")
444
+ parser.add_argument(
445
+ "--force-min-tokens",
446
+ type=int,
447
+ default=None,
448
+ help=(
449
+ "Stress-test only: suppress EOS until this many new tokens "
450
+ "(capped at each --lengths value)"
451
+ ),
452
+ )
453
+ args = parser.parse_args()
454
+
455
+ print("=" * 60)
456
+ print("R-SWA Cache Validation — Unlimited-OCR MLX")
457
+ print("=" * 60)
458
+
459
+ results = run_rswa_validation(
460
+ model_path=args.model_path,
461
+ image_path=args.image,
462
+ prompt=args.prompt,
463
+ test_lengths=args.lengths,
464
+ force_min_tokens=args.force_min_tokens,
465
+ )
466
+
467
+ args.output.parent.mkdir(parents=True, exist_ok=True)
468
+ args.output.write_text(
469
+ json.dumps(results, indent=2, ensure_ascii=False),
470
+ encoding="utf-8",
471
+ )
472
+
473
+ # Print summary
474
+ analysis = results["analysis"]
475
+ pass_cond = results["pass_conditions"]
476
+
477
+ print(f"\n{'='*60}")
478
+ print("R-SWA Validation Summary:")
479
+ cache_status = (
480
+ "PASS" if analysis["cache_appears_bounded"] is True
481
+ else "FAIL" if analysis["cache_appears_bounded"] is False
482
+ else "INCONCLUSIVE"
483
+ )
484
+ tps_status = (
485
+ "PASS" if analysis["tps_stable_over_length"] is True
486
+ else "FAIL" if analysis["tps_stable_over_length"] is False
487
+ else "INCONCLUSIVE"
488
+ )
489
+ print(f" Cache bounded: {cache_status}")
490
+ print(f" TPS stable: {tps_status}")
491
+ print(f" 8K test: {'PASS' if pass_cond['8k_test_passed'] else 'FAIL'}")
492
+ print(f" Max tokens: {analysis['max_successful_tokens']}")
493
+ print(f" Max repetition: {analysis['max_repetition_rate']:.4f}")
494
+ print(f"\nSaved to: {args.output}")
495
+ if not results["passed"]:
496
+ raise SystemExit(1)
497
+
498
+
499
+ if __name__ == "__main__":
500
+ main()
benchmarks/run_accuracy.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run accuracy benchmarks on evaluation images.
2
+
3
+ Usage:
4
+ python benchmarks/run_accuracy.py \
5
+ --model-path AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
6
+ --eval-dir ./benchmarks/eval_data/ \
7
+ --output benchmarks/results/accuracy_results.json
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ from collections import Counter
14
+ import json
15
+ import sys
16
+ import time
17
+ from pathlib import Path
18
+
19
+ # Add the project and src roots for both package and benchmark imports when the
20
+ # file is executed directly (``python benchmarks/run_accuracy.py``).
21
+ 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(
36
+ model_path: str,
37
+ eval_dir: Path,
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
+
44
+ Expects eval_dir structure:
45
+ eval_dir/
46
+ ├── images/ # Input images
47
+ └── ground_truth/ # Reference .txt files (same stem)
48
+ """
49
+ images_dir = eval_dir / "images"
50
+ gt_dir = eval_dir / "ground_truth"
51
+
52
+ if not images_dir.is_dir():
53
+ raise FileNotFoundError(f"Images directory not found: {images_dir}")
54
+ if not gt_dir.is_dir():
55
+ raise FileNotFoundError(f"Ground truth directory not found: {gt_dir}")
56
+
57
+ if not isinstance(prompt, str) or prompt.count("<image>") != 1:
58
+ raise ValueError("prompt must contain exactly one literal '<image>' token")
59
+ generation_profile = get_profile(profile)
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(
66
+ f for f in images_dir.iterdir()
67
+ if f.is_file() and f.suffix.lower() in {
68
+ ".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".bmp"
69
+ }
70
+ )
71
+
72
+ print(f"Found {len(image_files)} images")
73
+ if not image_files:
74
+ raise ValueError(f"No supported images found in: {images_dir}")
75
+
76
+ duplicate_stems = sorted(
77
+ stem for stem, count in Counter(path.stem for path in image_files).items()
78
+ if count > 1
79
+ )
80
+ if duplicate_stems:
81
+ raise ValueError(
82
+ "Multiple input images share the same stem: "
83
+ + ", ".join(duplicate_stems)
84
+ )
85
+
86
+ missing_ground_truth = [
87
+ image_file.name
88
+ for image_file in image_files
89
+ if not (gt_dir / f"{image_file.stem}.txt").is_file()
90
+ ]
91
+ if missing_ground_truth:
92
+ raise FileNotFoundError(
93
+ "Missing ground-truth text for: " + ", ".join(missing_ground_truth)
94
+ )
95
+
96
+ evaluation_files = [
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
109
+
110
+ for i, (img_file, gt_file) in enumerate(evaluation_files):
111
+ reference = normalize_ocr_output(gt_file.read_text(encoding="utf-8"))
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
+
132
+ # Extract text
133
+ if isinstance(response, str):
134
+ hypothesis = response
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
142
+ cer = compute_cer(reference, hypothesis)
143
+ wer = compute_wer(reference, hypothesis)
144
+ digit_cer = compute_digit_cer(reference, hypothesis)
145
+ cjk_cer = compute_cjk_cer(reference, hypothesis)
146
+ repetition = detect_repetition(hypothesis)
147
+ table_result = evaluate_tables(reference, hypothesis)
148
+ table_score = (
149
+ table_result["mean_score"]
150
+ if table_result["num_ref_tables"] > 0
151
+ else None
152
+ )
153
+
154
+ result = {
155
+ "file": img_file.name,
156
+ "cer": cer,
157
+ "wer": wer,
158
+ "digit_cer": digit_cer,
159
+ "cjk_cer": cjk_cer,
160
+ "repetition_rate": repetition,
161
+ "table_score": table_score,
162
+ "elapsed_seconds": elapsed,
163
+ "ref_length": len(reference),
164
+ "hyp_length": len(hypothesis),
165
+ "ref_digit_count": sum(
166
+ char.isascii() and char.isdigit() for char in reference
167
+ ),
168
+ "ref_cjk_count": len(extract_cjk_characters(reference)),
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
+
175
+ # Aggregate
176
+ import numpy as np
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,
196
+ "mean_wer": float(np.mean([r["wer"] for r in results])) if results else 0,
197
+ "num_digit_samples": len(digit_scores),
198
+ "num_cjk_samples": len(cjk_scores),
199
+ "mean_digit_cer": float(np.mean(digit_scores)) if digit_scores else None,
200
+ "mean_cjk_cer": float(np.mean(cjk_scores)) if cjk_scores else None,
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
+
207
+ return summary
208
+
209
+
210
+ def main():
211
+ parser = argparse.ArgumentParser(description="Run OCR accuracy benchmark")
212
+ parser.add_argument("--model-path", required=True)
213
+ parser.add_argument("--eval-dir", required=True, type=Path)
214
+ parser.add_argument("--output", type=Path, default=Path("benchmarks/results/accuracy_results.json"))
215
+ parser.add_argument("--prompt", default="<image>document parsing.")
216
+ parser.add_argument("--max-tokens", type=int, default=None,
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)
223
+ print("Unlimited-OCR Accuracy Benchmark")
224
+ print("=" * 60)
225
+
226
+ results = run_accuracy_benchmark(
227
+ model_path=args.model_path,
228
+ eval_dir=args.eval_dir,
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}"
240
+
241
+ print(f"\n{'='*60}")
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
+
248
+ if __name__ == "__main__":
249
+ main()
benchmarks/run_performance.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run performance benchmarks (decode TPS, end-to-end latency, and memory).
2
+
3
+ Usage:
4
+ python benchmarks/run_performance.py \
5
+ --model-path AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
6
+ --image test_image.jpg \
7
+ --output benchmarks/results/performance_results.json
8
+ """
9
+
10
+ from __future__ import annotations
11
+
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"))
22
+
23
+
24
+ def get_memory_usage_mb() -> float:
25
+ """Get the process peak resident-set size in MB."""
26
+ try:
27
+ import resource
28
+ usage = resource.getrusage(resource.RUSAGE_SELF)
29
+ # macOS reports bytes; Linux and most BSD-derived Python builds report
30
+ # KiB. The benchmark is Apple-focused, but avoid nonsensical values
31
+ # when its reporting helpers are run elsewhere.
32
+ divisor = 1024 * 1024 if sys.platform == "darwin" else 1024
33
+ return usage.ru_maxrss / divisor
34
+ except Exception:
35
+ return 0.0
36
+
37
+
38
+ def get_system_info() -> dict:
39
+ """Collect system information for reproducibility."""
40
+ info = {
41
+ "platform": platform.platform(),
42
+ "processor": platform.processor(),
43
+ "python_version": platform.python_version(),
44
+ "machine": platform.machine(),
45
+ }
46
+ for distribution, key in (("mlx", "mlx_version"), ("mlx-vlm", "mlx_vlm_version")):
47
+ try:
48
+ info[key] = importlib.metadata.version(distribution)
49
+ except importlib.metadata.PackageNotFoundError:
50
+ info[key] = None
51
+
52
+ # Try to get chip info on macOS
53
+ try:
54
+ import subprocess
55
+ result = subprocess.run(
56
+ ["sysctl", "-n", "machdep.cpu.brand_string"],
57
+ capture_output=True, text=True, timeout=5
58
+ )
59
+ if result.returncode == 0:
60
+ info["chip"] = result.stdout.strip()
61
+ except Exception:
62
+ pass
63
+
64
+ # Try to get memory info
65
+ try:
66
+ import subprocess
67
+ result = subprocess.run(
68
+ ["sysctl", "-n", "hw.memsize"],
69
+ capture_output=True, text=True, timeout=5
70
+ )
71
+ if result.returncode == 0:
72
+ info["total_memory_gb"] = int(result.stdout.strip()) / (1024**3)
73
+ except Exception:
74
+ pass
75
+
76
+ return info
77
+
78
+
79
+ def run_performance_benchmark(
80
+ model_path: str,
81
+ image_path: str,
82
+ prompt: str = "<image>document parsing.",
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():
89
+ raise FileNotFoundError(f"Test image not found: {image_path}")
90
+ if not isinstance(prompt, str) or prompt.count("<image>") != 1:
91
+ raise ValueError("prompt must contain exactly one literal '<image>' token")
92
+ if not isinstance(max_tokens, int) or isinstance(max_tokens, bool) or max_tokens < 1:
93
+ raise ValueError("max_tokens must be a positive integer")
94
+ if not isinstance(num_warmup, int) or isinstance(num_warmup, bool) or num_warmup < 0:
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
+
101
+ system_info = get_system_info()
102
+ total_memory = system_info.get("total_memory_gb")
103
+ memory_label = f"{total_memory:.0f} GB" if isinstance(total_memory, (int, float)) else "Unknown memory"
104
+ print(f"System: {system_info.get('chip', 'Unknown')} | {memory_label}")
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
+
112
+ print(f"Model load time: {load_time:.2f}s")
113
+ print(f"Memory after load: {mem_after_load:.0f} MB")
114
+
115
+ # Warmup runs
116
+ print(f"\nWarmup ({num_warmup} run(s))...")
117
+ for _ in range(num_warmup):
118
+ generate(model, processor, prompt=prompt, image=[image_path],
119
+ max_tokens=128, temperature=0.0, verbose=False)
120
+
121
+ import mlx.core as mx
122
+ mx.clear_cache()
123
+
124
+ # Benchmark runs
125
+ print(f"Benchmarking ({num_runs} runs, max_tokens={max_tokens})...")
126
+ runs = []
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
+
133
+ response = generate(
134
+ model, processor,
135
+ prompt=prompt,
136
+ image=[image_path],
137
+ max_tokens=max_tokens,
138
+ temperature=0.0,
139
+ verbose=False,
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):
147
+ text = response
148
+ tokens = len(text.split())
149
+ token_count_source = "whitespace estimate"
150
+ elif hasattr(response, "text"):
151
+ text = response.text
152
+ if not isinstance(text, str):
153
+ raise TypeError("mlx-vlm returned a non-string text field")
154
+ raw_tokens = getattr(
155
+ response,
156
+ "generation_tokens",
157
+ getattr(response, "tokens_generated", None),
158
+ )
159
+ if raw_tokens is None:
160
+ tokens = len(text.split())
161
+ token_count_source = "whitespace estimate"
162
+ else:
163
+ tokens = raw_tokens
164
+ token_count_source = "mlx-vlm token count"
165
+ elif isinstance(response, dict):
166
+ text = response.get("text", "")
167
+ if not isinstance(text, str):
168
+ raise TypeError("mlx-vlm returned a non-string text field")
169
+ raw_tokens = response.get(
170
+ "generation_tokens", response.get("tokens_generated")
171
+ )
172
+ if raw_tokens is None:
173
+ tokens = len(text.split())
174
+ token_count_source = "whitespace estimate"
175
+ else:
176
+ tokens = raw_tokens
177
+ token_count_source = "mlx-vlm token count"
178
+ else:
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,
203
+ "tokens_generated": tokens,
204
+ "tokens_generated_source": token_count_source,
205
+ "tokens_per_second": tps,
206
+ "tokens_per_second_source": tps_source,
207
+ "peak_memory_mb": mlx_peak_mb,
208
+ "memory_delta_mb": max(0.0, mlx_peak_mb - active_memory_before_mb),
209
+ "process_peak_rss_mb": process_peak_mb,
210
+ "prompt_tokens": int(getattr(response, "prompt_tokens", 0) or 0),
211
+ "prompt_tokens_per_second": float(
212
+ getattr(response, "prompt_tps", 0.0) or 0.0
213
+ ),
214
+ "finish_reason": getattr(response, "finish_reason", None),
215
+ }
216
+ runs.append(run_result)
217
+ print(f" Run {i+1}: {elapsed:.2f}s | {tokens} tokens | {tps:.1f} tok/s | "
218
+ f"{mlx_peak_mb:.0f} MB")
219
+
220
+ # Aggregate
221
+ import numpy as np
222
+ tps_values = [r["tokens_per_second"] for r in runs]
223
+ elapsed_values = [r["elapsed_seconds"] for r in runs]
224
+ mem_values = [r["peak_memory_mb"] for r in runs]
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,
231
+ "system": system_info,
232
+ "model_load_time_seconds": load_time,
233
+ "memory_after_load_mb": mem_after_load,
234
+ "num_warmup": num_warmup,
235
+ "num_runs": num_runs,
236
+ "mean_tps": float(np.mean(tps_values)) if tps_values else 0,
237
+ "std_tps": float(np.std(tps_values)) if tps_values else 0,
238
+ "mean_elapsed_seconds": float(np.mean(elapsed_values)),
239
+ "mean_peak_memory_mb": float(np.mean(mem_values)),
240
+ "runs": runs,
241
+ }
242
+
243
+ return results
244
+
245
+
246
+ def main():
247
+ parser = argparse.ArgumentParser(description="Run OCR performance benchmark")
248
+ parser.add_argument("--model-path", required=True)
249
+ parser.add_argument("--image", required=True, help="Test image path")
250
+ parser.add_argument("--output", type=Path, default=Path("benchmarks/results/performance_results.json"))
251
+ parser.add_argument("--prompt", default="<image>document parsing.")
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)
258
+ print("Unlimited-OCR Performance Benchmark")
259
+ print("=" * 60)
260
+
261
+ results = run_performance_benchmark(
262
+ model_path=args.model_path,
263
+ image_path=args.image,
264
+ prompt=args.prompt,
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)
271
+ args.output.write_text(json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8")
272
+
273
+ print(f"\n{'='*60}")
274
+ print(f"Mean TPS: {results['mean_tps']:.1f} | "
275
+ f"Peak Memory: {results['mean_peak_memory_mb']:.0f} MB")
276
+ print(f"Saved to: {args.output}")
277
+
278
+
279
+ if __name__ == "__main__":
280
+ main()
chat_template.jinja ADDED
@@ -0,0 +1 @@
 
 
1
+ {% for message in messages %}{% if message['role'] == 'user' %}{% elif message['role'] == 'assistant' %}{% endif %}{{message['content']}} {% endfor %}{% if add_generation_prompt %}{% endif %}
config.json ADDED
@@ -0,0 +1,1321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "UnlimitedOCRForCausalLM"
4
+ ],
5
+ "bos_token_id": 0,
6
+ "candidate_resolutions": [
7
+ [
8
+ 1024,
9
+ 1024
10
+ ]
11
+ ],
12
+ "eos_token_id": 1,
13
+ "first_k_dense_replace": 1,
14
+ "global_view_pos": "head",
15
+ "hidden_size": 1280,
16
+ "intermediate_size": 6848,
17
+ "kv_lora_rank": null,
18
+ "language_config": {
19
+ "architectures": [
20
+ "DeepseekOCRForCausalLM"
21
+ ],
22
+ "bos_token_id": 0,
23
+ "eos_token_id": 1,
24
+ "first_k_dense_replace": 1,
25
+ "hidden_size": 1280,
26
+ "intermediate_size": 6848,
27
+ "kv_lora_rank": null,
28
+ "lm_head": true,
29
+ "max_position_embeddings": 32768,
30
+ "moe_intermediate_size": 896,
31
+ "n_group": 1,
32
+ "n_routed_experts": 64,
33
+ "n_shared_experts": 2,
34
+ "num_attention_heads": 10,
35
+ "num_experts_per_tok": 6,
36
+ "num_hidden_layers": 12,
37
+ "num_key_value_heads": 10,
38
+ "q_lora_rank": null,
39
+ "qk_nope_head_dim": 0,
40
+ "qk_rope_head_dim": 0,
41
+ "rm_head": false,
42
+ "topk_group": 1,
43
+ "topk_method": "greedy",
44
+ "torch_dtype": "bfloat16",
45
+ "use_mla": false,
46
+ "v_head_dim": 128,
47
+ "vocab_size": 129280,
48
+ "sliding_window_size": 128,
49
+ "sliding_window": 128
50
+ },
51
+ "lm_head": true,
52
+ "max_position_embeddings": 32768,
53
+ "model_type": "unlimited-ocr",
54
+ "moe_intermediate_size": 896,
55
+ "n_group": 1,
56
+ "n_routed_experts": 64,
57
+ "n_shared_experts": 2,
58
+ "num_attention_heads": 10,
59
+ "num_experts_per_tok": 6,
60
+ "num_hidden_layers": 12,
61
+ "num_key_value_heads": 10,
62
+ "projector_config": {
63
+ "input_dim": 2048,
64
+ "model_type": "mlp_projector",
65
+ "n_embed": 1280,
66
+ "projector_type": "linear"
67
+ },
68
+ "q_lora_rank": null,
69
+ "qk_nope_head_dim": 0,
70
+ "qk_rope_head_dim": 0,
71
+ "quantization": {
72
+ "group_size": 32,
73
+ "bits": 8,
74
+ "mode": "mxfp8",
75
+ "language_model.model.embed_tokens": {
76
+ "group_size": 32,
77
+ "bits": 8,
78
+ "mode": "mxfp8"
79
+ },
80
+ "language_model.model.layers.0.self_attn.q_proj": {
81
+ "group_size": 32,
82
+ "bits": 8,
83
+ "mode": "mxfp8"
84
+ },
85
+ "language_model.model.layers.0.self_attn.k_proj": {
86
+ "group_size": 32,
87
+ "bits": 8,
88
+ "mode": "mxfp8"
89
+ },
90
+ "language_model.model.layers.0.self_attn.v_proj": {
91
+ "group_size": 32,
92
+ "bits": 8,
93
+ "mode": "mxfp8"
94
+ },
95
+ "language_model.model.layers.0.self_attn.o_proj": {
96
+ "group_size": 32,
97
+ "bits": 8,
98
+ "mode": "mxfp8"
99
+ },
100
+ "language_model.model.layers.0.mlp.gate_proj": {
101
+ "group_size": 32,
102
+ "bits": 8,
103
+ "mode": "mxfp8"
104
+ },
105
+ "language_model.model.layers.0.mlp.up_proj": {
106
+ "group_size": 32,
107
+ "bits": 8,
108
+ "mode": "mxfp8"
109
+ },
110
+ "language_model.model.layers.0.mlp.down_proj": {
111
+ "group_size": 32,
112
+ "bits": 8,
113
+ "mode": "mxfp8"
114
+ },
115
+ "language_model.model.layers.1.self_attn.q_proj": {
116
+ "group_size": 32,
117
+ "bits": 8,
118
+ "mode": "mxfp8"
119
+ },
120
+ "language_model.model.layers.1.self_attn.k_proj": {
121
+ "group_size": 32,
122
+ "bits": 8,
123
+ "mode": "mxfp8"
124
+ },
125
+ "language_model.model.layers.1.self_attn.v_proj": {
126
+ "group_size": 32,
127
+ "bits": 8,
128
+ "mode": "mxfp8"
129
+ },
130
+ "language_model.model.layers.1.self_attn.o_proj": {
131
+ "group_size": 32,
132
+ "bits": 8,
133
+ "mode": "mxfp8"
134
+ },
135
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj": {
136
+ "group_size": 32,
137
+ "bits": 8,
138
+ "mode": "mxfp8"
139
+ },
140
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj": {
141
+ "group_size": 32,
142
+ "bits": 8,
143
+ "mode": "mxfp8"
144
+ },
145
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj": {
146
+ "group_size": 32,
147
+ "bits": 8,
148
+ "mode": "mxfp8"
149
+ },
150
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj": {
151
+ "group_size": 32,
152
+ "bits": 8,
153
+ "mode": "mxfp8"
154
+ },
155
+ "language_model.model.layers.1.mlp.shared_experts.up_proj": {
156
+ "group_size": 32,
157
+ "bits": 8,
158
+ "mode": "mxfp8"
159
+ },
160
+ "language_model.model.layers.1.mlp.shared_experts.down_proj": {
161
+ "group_size": 32,
162
+ "bits": 8,
163
+ "mode": "mxfp8"
164
+ },
165
+ "language_model.model.layers.2.self_attn.q_proj": {
166
+ "group_size": 32,
167
+ "bits": 8,
168
+ "mode": "mxfp8"
169
+ },
170
+ "language_model.model.layers.2.self_attn.k_proj": {
171
+ "group_size": 32,
172
+ "bits": 8,
173
+ "mode": "mxfp8"
174
+ },
175
+ "language_model.model.layers.2.self_attn.v_proj": {
176
+ "group_size": 32,
177
+ "bits": 8,
178
+ "mode": "mxfp8"
179
+ },
180
+ "language_model.model.layers.2.self_attn.o_proj": {
181
+ "group_size": 32,
182
+ "bits": 8,
183
+ "mode": "mxfp8"
184
+ },
185
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj": {
186
+ "group_size": 32,
187
+ "bits": 8,
188
+ "mode": "mxfp8"
189
+ },
190
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj": {
191
+ "group_size": 32,
192
+ "bits": 8,
193
+ "mode": "mxfp8"
194
+ },
195
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj": {
196
+ "group_size": 32,
197
+ "bits": 8,
198
+ "mode": "mxfp8"
199
+ },
200
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj": {
201
+ "group_size": 32,
202
+ "bits": 8,
203
+ "mode": "mxfp8"
204
+ },
205
+ "language_model.model.layers.2.mlp.shared_experts.up_proj": {
206
+ "group_size": 32,
207
+ "bits": 8,
208
+ "mode": "mxfp8"
209
+ },
210
+ "language_model.model.layers.2.mlp.shared_experts.down_proj": {
211
+ "group_size": 32,
212
+ "bits": 8,
213
+ "mode": "mxfp8"
214
+ },
215
+ "language_model.model.layers.3.self_attn.q_proj": {
216
+ "group_size": 32,
217
+ "bits": 8,
218
+ "mode": "mxfp8"
219
+ },
220
+ "language_model.model.layers.3.self_attn.k_proj": {
221
+ "group_size": 32,
222
+ "bits": 8,
223
+ "mode": "mxfp8"
224
+ },
225
+ "language_model.model.layers.3.self_attn.v_proj": {
226
+ "group_size": 32,
227
+ "bits": 8,
228
+ "mode": "mxfp8"
229
+ },
230
+ "language_model.model.layers.3.self_attn.o_proj": {
231
+ "group_size": 32,
232
+ "bits": 8,
233
+ "mode": "mxfp8"
234
+ },
235
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj": {
236
+ "group_size": 32,
237
+ "bits": 8,
238
+ "mode": "mxfp8"
239
+ },
240
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj": {
241
+ "group_size": 32,
242
+ "bits": 8,
243
+ "mode": "mxfp8"
244
+ },
245
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj": {
246
+ "group_size": 32,
247
+ "bits": 8,
248
+ "mode": "mxfp8"
249
+ },
250
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj": {
251
+ "group_size": 32,
252
+ "bits": 8,
253
+ "mode": "mxfp8"
254
+ },
255
+ "language_model.model.layers.3.mlp.shared_experts.up_proj": {
256
+ "group_size": 32,
257
+ "bits": 8,
258
+ "mode": "mxfp8"
259
+ },
260
+ "language_model.model.layers.3.mlp.shared_experts.down_proj": {
261
+ "group_size": 32,
262
+ "bits": 8,
263
+ "mode": "mxfp8"
264
+ },
265
+ "language_model.model.layers.4.self_attn.q_proj": {
266
+ "group_size": 32,
267
+ "bits": 8,
268
+ "mode": "mxfp8"
269
+ },
270
+ "language_model.model.layers.4.self_attn.k_proj": {
271
+ "group_size": 32,
272
+ "bits": 8,
273
+ "mode": "mxfp8"
274
+ },
275
+ "language_model.model.layers.4.self_attn.v_proj": {
276
+ "group_size": 32,
277
+ "bits": 8,
278
+ "mode": "mxfp8"
279
+ },
280
+ "language_model.model.layers.4.self_attn.o_proj": {
281
+ "group_size": 32,
282
+ "bits": 8,
283
+ "mode": "mxfp8"
284
+ },
285
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj": {
286
+ "group_size": 32,
287
+ "bits": 8,
288
+ "mode": "mxfp8"
289
+ },
290
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj": {
291
+ "group_size": 32,
292
+ "bits": 8,
293
+ "mode": "mxfp8"
294
+ },
295
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj": {
296
+ "group_size": 32,
297
+ "bits": 8,
298
+ "mode": "mxfp8"
299
+ },
300
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj": {
301
+ "group_size": 32,
302
+ "bits": 8,
303
+ "mode": "mxfp8"
304
+ },
305
+ "language_model.model.layers.4.mlp.shared_experts.up_proj": {
306
+ "group_size": 32,
307
+ "bits": 8,
308
+ "mode": "mxfp8"
309
+ },
310
+ "language_model.model.layers.4.mlp.shared_experts.down_proj": {
311
+ "group_size": 32,
312
+ "bits": 8,
313
+ "mode": "mxfp8"
314
+ },
315
+ "language_model.model.layers.5.self_attn.q_proj": {
316
+ "group_size": 32,
317
+ "bits": 8,
318
+ "mode": "mxfp8"
319
+ },
320
+ "language_model.model.layers.5.self_attn.k_proj": {
321
+ "group_size": 32,
322
+ "bits": 8,
323
+ "mode": "mxfp8"
324
+ },
325
+ "language_model.model.layers.5.self_attn.v_proj": {
326
+ "group_size": 32,
327
+ "bits": 8,
328
+ "mode": "mxfp8"
329
+ },
330
+ "language_model.model.layers.5.self_attn.o_proj": {
331
+ "group_size": 32,
332
+ "bits": 8,
333
+ "mode": "mxfp8"
334
+ },
335
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj": {
336
+ "group_size": 32,
337
+ "bits": 8,
338
+ "mode": "mxfp8"
339
+ },
340
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj": {
341
+ "group_size": 32,
342
+ "bits": 8,
343
+ "mode": "mxfp8"
344
+ },
345
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj": {
346
+ "group_size": 32,
347
+ "bits": 8,
348
+ "mode": "mxfp8"
349
+ },
350
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj": {
351
+ "group_size": 32,
352
+ "bits": 8,
353
+ "mode": "mxfp8"
354
+ },
355
+ "language_model.model.layers.5.mlp.shared_experts.up_proj": {
356
+ "group_size": 32,
357
+ "bits": 8,
358
+ "mode": "mxfp8"
359
+ },
360
+ "language_model.model.layers.5.mlp.shared_experts.down_proj": {
361
+ "group_size": 32,
362
+ "bits": 8,
363
+ "mode": "mxfp8"
364
+ },
365
+ "language_model.model.layers.6.self_attn.q_proj": {
366
+ "group_size": 32,
367
+ "bits": 8,
368
+ "mode": "mxfp8"
369
+ },
370
+ "language_model.model.layers.6.self_attn.k_proj": {
371
+ "group_size": 32,
372
+ "bits": 8,
373
+ "mode": "mxfp8"
374
+ },
375
+ "language_model.model.layers.6.self_attn.v_proj": {
376
+ "group_size": 32,
377
+ "bits": 8,
378
+ "mode": "mxfp8"
379
+ },
380
+ "language_model.model.layers.6.self_attn.o_proj": {
381
+ "group_size": 32,
382
+ "bits": 8,
383
+ "mode": "mxfp8"
384
+ },
385
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj": {
386
+ "group_size": 32,
387
+ "bits": 8,
388
+ "mode": "mxfp8"
389
+ },
390
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj": {
391
+ "group_size": 32,
392
+ "bits": 8,
393
+ "mode": "mxfp8"
394
+ },
395
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj": {
396
+ "group_size": 32,
397
+ "bits": 8,
398
+ "mode": "mxfp8"
399
+ },
400
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj": {
401
+ "group_size": 32,
402
+ "bits": 8,
403
+ "mode": "mxfp8"
404
+ },
405
+ "language_model.model.layers.6.mlp.shared_experts.up_proj": {
406
+ "group_size": 32,
407
+ "bits": 8,
408
+ "mode": "mxfp8"
409
+ },
410
+ "language_model.model.layers.6.mlp.shared_experts.down_proj": {
411
+ "group_size": 32,
412
+ "bits": 8,
413
+ "mode": "mxfp8"
414
+ },
415
+ "language_model.model.layers.7.self_attn.q_proj": {
416
+ "group_size": 32,
417
+ "bits": 8,
418
+ "mode": "mxfp8"
419
+ },
420
+ "language_model.model.layers.7.self_attn.k_proj": {
421
+ "group_size": 32,
422
+ "bits": 8,
423
+ "mode": "mxfp8"
424
+ },
425
+ "language_model.model.layers.7.self_attn.v_proj": {
426
+ "group_size": 32,
427
+ "bits": 8,
428
+ "mode": "mxfp8"
429
+ },
430
+ "language_model.model.layers.7.self_attn.o_proj": {
431
+ "group_size": 32,
432
+ "bits": 8,
433
+ "mode": "mxfp8"
434
+ },
435
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj": {
436
+ "group_size": 32,
437
+ "bits": 8,
438
+ "mode": "mxfp8"
439
+ },
440
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj": {
441
+ "group_size": 32,
442
+ "bits": 8,
443
+ "mode": "mxfp8"
444
+ },
445
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj": {
446
+ "group_size": 32,
447
+ "bits": 8,
448
+ "mode": "mxfp8"
449
+ },
450
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj": {
451
+ "group_size": 32,
452
+ "bits": 8,
453
+ "mode": "mxfp8"
454
+ },
455
+ "language_model.model.layers.7.mlp.shared_experts.up_proj": {
456
+ "group_size": 32,
457
+ "bits": 8,
458
+ "mode": "mxfp8"
459
+ },
460
+ "language_model.model.layers.7.mlp.shared_experts.down_proj": {
461
+ "group_size": 32,
462
+ "bits": 8,
463
+ "mode": "mxfp8"
464
+ },
465
+ "language_model.model.layers.8.self_attn.q_proj": {
466
+ "group_size": 32,
467
+ "bits": 8,
468
+ "mode": "mxfp8"
469
+ },
470
+ "language_model.model.layers.8.self_attn.k_proj": {
471
+ "group_size": 32,
472
+ "bits": 8,
473
+ "mode": "mxfp8"
474
+ },
475
+ "language_model.model.layers.8.self_attn.v_proj": {
476
+ "group_size": 32,
477
+ "bits": 8,
478
+ "mode": "mxfp8"
479
+ },
480
+ "language_model.model.layers.8.self_attn.o_proj": {
481
+ "group_size": 32,
482
+ "bits": 8,
483
+ "mode": "mxfp8"
484
+ },
485
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj": {
486
+ "group_size": 32,
487
+ "bits": 8,
488
+ "mode": "mxfp8"
489
+ },
490
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj": {
491
+ "group_size": 32,
492
+ "bits": 8,
493
+ "mode": "mxfp8"
494
+ },
495
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj": {
496
+ "group_size": 32,
497
+ "bits": 8,
498
+ "mode": "mxfp8"
499
+ },
500
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj": {
501
+ "group_size": 32,
502
+ "bits": 8,
503
+ "mode": "mxfp8"
504
+ },
505
+ "language_model.model.layers.8.mlp.shared_experts.up_proj": {
506
+ "group_size": 32,
507
+ "bits": 8,
508
+ "mode": "mxfp8"
509
+ },
510
+ "language_model.model.layers.8.mlp.shared_experts.down_proj": {
511
+ "group_size": 32,
512
+ "bits": 8,
513
+ "mode": "mxfp8"
514
+ },
515
+ "language_model.model.layers.9.self_attn.q_proj": {
516
+ "group_size": 32,
517
+ "bits": 8,
518
+ "mode": "mxfp8"
519
+ },
520
+ "language_model.model.layers.9.self_attn.k_proj": {
521
+ "group_size": 32,
522
+ "bits": 8,
523
+ "mode": "mxfp8"
524
+ },
525
+ "language_model.model.layers.9.self_attn.v_proj": {
526
+ "group_size": 32,
527
+ "bits": 8,
528
+ "mode": "mxfp8"
529
+ },
530
+ "language_model.model.layers.9.self_attn.o_proj": {
531
+ "group_size": 32,
532
+ "bits": 8,
533
+ "mode": "mxfp8"
534
+ },
535
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj": {
536
+ "group_size": 32,
537
+ "bits": 8,
538
+ "mode": "mxfp8"
539
+ },
540
+ "language_model.model.layers.9.mlp.switch_mlp.up_proj": {
541
+ "group_size": 32,
542
+ "bits": 8,
543
+ "mode": "mxfp8"
544
+ },
545
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj": {
546
+ "group_size": 32,
547
+ "bits": 8,
548
+ "mode": "mxfp8"
549
+ },
550
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj": {
551
+ "group_size": 32,
552
+ "bits": 8,
553
+ "mode": "mxfp8"
554
+ },
555
+ "language_model.model.layers.9.mlp.shared_experts.up_proj": {
556
+ "group_size": 32,
557
+ "bits": 8,
558
+ "mode": "mxfp8"
559
+ },
560
+ "language_model.model.layers.9.mlp.shared_experts.down_proj": {
561
+ "group_size": 32,
562
+ "bits": 8,
563
+ "mode": "mxfp8"
564
+ },
565
+ "language_model.model.layers.10.self_attn.q_proj": {
566
+ "group_size": 32,
567
+ "bits": 8,
568
+ "mode": "mxfp8"
569
+ },
570
+ "language_model.model.layers.10.self_attn.k_proj": {
571
+ "group_size": 32,
572
+ "bits": 8,
573
+ "mode": "mxfp8"
574
+ },
575
+ "language_model.model.layers.10.self_attn.v_proj": {
576
+ "group_size": 32,
577
+ "bits": 8,
578
+ "mode": "mxfp8"
579
+ },
580
+ "language_model.model.layers.10.self_attn.o_proj": {
581
+ "group_size": 32,
582
+ "bits": 8,
583
+ "mode": "mxfp8"
584
+ },
585
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj": {
586
+ "group_size": 32,
587
+ "bits": 8,
588
+ "mode": "mxfp8"
589
+ },
590
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj": {
591
+ "group_size": 32,
592
+ "bits": 8,
593
+ "mode": "mxfp8"
594
+ },
595
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj": {
596
+ "group_size": 32,
597
+ "bits": 8,
598
+ "mode": "mxfp8"
599
+ },
600
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj": {
601
+ "group_size": 32,
602
+ "bits": 8,
603
+ "mode": "mxfp8"
604
+ },
605
+ "language_model.model.layers.10.mlp.shared_experts.up_proj": {
606
+ "group_size": 32,
607
+ "bits": 8,
608
+ "mode": "mxfp8"
609
+ },
610
+ "language_model.model.layers.10.mlp.shared_experts.down_proj": {
611
+ "group_size": 32,
612
+ "bits": 8,
613
+ "mode": "mxfp8"
614
+ },
615
+ "language_model.model.layers.11.self_attn.q_proj": {
616
+ "group_size": 32,
617
+ "bits": 8,
618
+ "mode": "mxfp8"
619
+ },
620
+ "language_model.model.layers.11.self_attn.k_proj": {
621
+ "group_size": 32,
622
+ "bits": 8,
623
+ "mode": "mxfp8"
624
+ },
625
+ "language_model.model.layers.11.self_attn.v_proj": {
626
+ "group_size": 32,
627
+ "bits": 8,
628
+ "mode": "mxfp8"
629
+ },
630
+ "language_model.model.layers.11.self_attn.o_proj": {
631
+ "group_size": 32,
632
+ "bits": 8,
633
+ "mode": "mxfp8"
634
+ },
635
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj": {
636
+ "group_size": 32,
637
+ "bits": 8,
638
+ "mode": "mxfp8"
639
+ },
640
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj": {
641
+ "group_size": 32,
642
+ "bits": 8,
643
+ "mode": "mxfp8"
644
+ },
645
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj": {
646
+ "group_size": 32,
647
+ "bits": 8,
648
+ "mode": "mxfp8"
649
+ },
650
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj": {
651
+ "group_size": 32,
652
+ "bits": 8,
653
+ "mode": "mxfp8"
654
+ },
655
+ "language_model.model.layers.11.mlp.shared_experts.up_proj": {
656
+ "group_size": 32,
657
+ "bits": 8,
658
+ "mode": "mxfp8"
659
+ },
660
+ "language_model.model.layers.11.mlp.shared_experts.down_proj": {
661
+ "group_size": 32,
662
+ "bits": 8,
663
+ "mode": "mxfp8"
664
+ },
665
+ "language_model.lm_head": {
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": {
677
+ "group_size": 32,
678
+ "bits": 8,
679
+ "mode": "mxfp8",
680
+ "language_model.model.embed_tokens": {
681
+ "group_size": 32,
682
+ "bits": 8,
683
+ "mode": "mxfp8"
684
+ },
685
+ "language_model.model.layers.0.self_attn.q_proj": {
686
+ "group_size": 32,
687
+ "bits": 8,
688
+ "mode": "mxfp8"
689
+ },
690
+ "language_model.model.layers.0.self_attn.k_proj": {
691
+ "group_size": 32,
692
+ "bits": 8,
693
+ "mode": "mxfp8"
694
+ },
695
+ "language_model.model.layers.0.self_attn.v_proj": {
696
+ "group_size": 32,
697
+ "bits": 8,
698
+ "mode": "mxfp8"
699
+ },
700
+ "language_model.model.layers.0.self_attn.o_proj": {
701
+ "group_size": 32,
702
+ "bits": 8,
703
+ "mode": "mxfp8"
704
+ },
705
+ "language_model.model.layers.0.mlp.gate_proj": {
706
+ "group_size": 32,
707
+ "bits": 8,
708
+ "mode": "mxfp8"
709
+ },
710
+ "language_model.model.layers.0.mlp.up_proj": {
711
+ "group_size": 32,
712
+ "bits": 8,
713
+ "mode": "mxfp8"
714
+ },
715
+ "language_model.model.layers.0.mlp.down_proj": {
716
+ "group_size": 32,
717
+ "bits": 8,
718
+ "mode": "mxfp8"
719
+ },
720
+ "language_model.model.layers.1.self_attn.q_proj": {
721
+ "group_size": 32,
722
+ "bits": 8,
723
+ "mode": "mxfp8"
724
+ },
725
+ "language_model.model.layers.1.self_attn.k_proj": {
726
+ "group_size": 32,
727
+ "bits": 8,
728
+ "mode": "mxfp8"
729
+ },
730
+ "language_model.model.layers.1.self_attn.v_proj": {
731
+ "group_size": 32,
732
+ "bits": 8,
733
+ "mode": "mxfp8"
734
+ },
735
+ "language_model.model.layers.1.self_attn.o_proj": {
736
+ "group_size": 32,
737
+ "bits": 8,
738
+ "mode": "mxfp8"
739
+ },
740
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj": {
741
+ "group_size": 32,
742
+ "bits": 8,
743
+ "mode": "mxfp8"
744
+ },
745
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj": {
746
+ "group_size": 32,
747
+ "bits": 8,
748
+ "mode": "mxfp8"
749
+ },
750
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj": {
751
+ "group_size": 32,
752
+ "bits": 8,
753
+ "mode": "mxfp8"
754
+ },
755
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj": {
756
+ "group_size": 32,
757
+ "bits": 8,
758
+ "mode": "mxfp8"
759
+ },
760
+ "language_model.model.layers.1.mlp.shared_experts.up_proj": {
761
+ "group_size": 32,
762
+ "bits": 8,
763
+ "mode": "mxfp8"
764
+ },
765
+ "language_model.model.layers.1.mlp.shared_experts.down_proj": {
766
+ "group_size": 32,
767
+ "bits": 8,
768
+ "mode": "mxfp8"
769
+ },
770
+ "language_model.model.layers.2.self_attn.q_proj": {
771
+ "group_size": 32,
772
+ "bits": 8,
773
+ "mode": "mxfp8"
774
+ },
775
+ "language_model.model.layers.2.self_attn.k_proj": {
776
+ "group_size": 32,
777
+ "bits": 8,
778
+ "mode": "mxfp8"
779
+ },
780
+ "language_model.model.layers.2.self_attn.v_proj": {
781
+ "group_size": 32,
782
+ "bits": 8,
783
+ "mode": "mxfp8"
784
+ },
785
+ "language_model.model.layers.2.self_attn.o_proj": {
786
+ "group_size": 32,
787
+ "bits": 8,
788
+ "mode": "mxfp8"
789
+ },
790
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj": {
791
+ "group_size": 32,
792
+ "bits": 8,
793
+ "mode": "mxfp8"
794
+ },
795
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj": {
796
+ "group_size": 32,
797
+ "bits": 8,
798
+ "mode": "mxfp8"
799
+ },
800
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj": {
801
+ "group_size": 32,
802
+ "bits": 8,
803
+ "mode": "mxfp8"
804
+ },
805
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj": {
806
+ "group_size": 32,
807
+ "bits": 8,
808
+ "mode": "mxfp8"
809
+ },
810
+ "language_model.model.layers.2.mlp.shared_experts.up_proj": {
811
+ "group_size": 32,
812
+ "bits": 8,
813
+ "mode": "mxfp8"
814
+ },
815
+ "language_model.model.layers.2.mlp.shared_experts.down_proj": {
816
+ "group_size": 32,
817
+ "bits": 8,
818
+ "mode": "mxfp8"
819
+ },
820
+ "language_model.model.layers.3.self_attn.q_proj": {
821
+ "group_size": 32,
822
+ "bits": 8,
823
+ "mode": "mxfp8"
824
+ },
825
+ "language_model.model.layers.3.self_attn.k_proj": {
826
+ "group_size": 32,
827
+ "bits": 8,
828
+ "mode": "mxfp8"
829
+ },
830
+ "language_model.model.layers.3.self_attn.v_proj": {
831
+ "group_size": 32,
832
+ "bits": 8,
833
+ "mode": "mxfp8"
834
+ },
835
+ "language_model.model.layers.3.self_attn.o_proj": {
836
+ "group_size": 32,
837
+ "bits": 8,
838
+ "mode": "mxfp8"
839
+ },
840
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj": {
841
+ "group_size": 32,
842
+ "bits": 8,
843
+ "mode": "mxfp8"
844
+ },
845
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj": {
846
+ "group_size": 32,
847
+ "bits": 8,
848
+ "mode": "mxfp8"
849
+ },
850
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj": {
851
+ "group_size": 32,
852
+ "bits": 8,
853
+ "mode": "mxfp8"
854
+ },
855
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj": {
856
+ "group_size": 32,
857
+ "bits": 8,
858
+ "mode": "mxfp8"
859
+ },
860
+ "language_model.model.layers.3.mlp.shared_experts.up_proj": {
861
+ "group_size": 32,
862
+ "bits": 8,
863
+ "mode": "mxfp8"
864
+ },
865
+ "language_model.model.layers.3.mlp.shared_experts.down_proj": {
866
+ "group_size": 32,
867
+ "bits": 8,
868
+ "mode": "mxfp8"
869
+ },
870
+ "language_model.model.layers.4.self_attn.q_proj": {
871
+ "group_size": 32,
872
+ "bits": 8,
873
+ "mode": "mxfp8"
874
+ },
875
+ "language_model.model.layers.4.self_attn.k_proj": {
876
+ "group_size": 32,
877
+ "bits": 8,
878
+ "mode": "mxfp8"
879
+ },
880
+ "language_model.model.layers.4.self_attn.v_proj": {
881
+ "group_size": 32,
882
+ "bits": 8,
883
+ "mode": "mxfp8"
884
+ },
885
+ "language_model.model.layers.4.self_attn.o_proj": {
886
+ "group_size": 32,
887
+ "bits": 8,
888
+ "mode": "mxfp8"
889
+ },
890
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj": {
891
+ "group_size": 32,
892
+ "bits": 8,
893
+ "mode": "mxfp8"
894
+ },
895
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj": {
896
+ "group_size": 32,
897
+ "bits": 8,
898
+ "mode": "mxfp8"
899
+ },
900
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj": {
901
+ "group_size": 32,
902
+ "bits": 8,
903
+ "mode": "mxfp8"
904
+ },
905
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj": {
906
+ "group_size": 32,
907
+ "bits": 8,
908
+ "mode": "mxfp8"
909
+ },
910
+ "language_model.model.layers.4.mlp.shared_experts.up_proj": {
911
+ "group_size": 32,
912
+ "bits": 8,
913
+ "mode": "mxfp8"
914
+ },
915
+ "language_model.model.layers.4.mlp.shared_experts.down_proj": {
916
+ "group_size": 32,
917
+ "bits": 8,
918
+ "mode": "mxfp8"
919
+ },
920
+ "language_model.model.layers.5.self_attn.q_proj": {
921
+ "group_size": 32,
922
+ "bits": 8,
923
+ "mode": "mxfp8"
924
+ },
925
+ "language_model.model.layers.5.self_attn.k_proj": {
926
+ "group_size": 32,
927
+ "bits": 8,
928
+ "mode": "mxfp8"
929
+ },
930
+ "language_model.model.layers.5.self_attn.v_proj": {
931
+ "group_size": 32,
932
+ "bits": 8,
933
+ "mode": "mxfp8"
934
+ },
935
+ "language_model.model.layers.5.self_attn.o_proj": {
936
+ "group_size": 32,
937
+ "bits": 8,
938
+ "mode": "mxfp8"
939
+ },
940
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj": {
941
+ "group_size": 32,
942
+ "bits": 8,
943
+ "mode": "mxfp8"
944
+ },
945
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj": {
946
+ "group_size": 32,
947
+ "bits": 8,
948
+ "mode": "mxfp8"
949
+ },
950
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj": {
951
+ "group_size": 32,
952
+ "bits": 8,
953
+ "mode": "mxfp8"
954
+ },
955
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj": {
956
+ "group_size": 32,
957
+ "bits": 8,
958
+ "mode": "mxfp8"
959
+ },
960
+ "language_model.model.layers.5.mlp.shared_experts.up_proj": {
961
+ "group_size": 32,
962
+ "bits": 8,
963
+ "mode": "mxfp8"
964
+ },
965
+ "language_model.model.layers.5.mlp.shared_experts.down_proj": {
966
+ "group_size": 32,
967
+ "bits": 8,
968
+ "mode": "mxfp8"
969
+ },
970
+ "language_model.model.layers.6.self_attn.q_proj": {
971
+ "group_size": 32,
972
+ "bits": 8,
973
+ "mode": "mxfp8"
974
+ },
975
+ "language_model.model.layers.6.self_attn.k_proj": {
976
+ "group_size": 32,
977
+ "bits": 8,
978
+ "mode": "mxfp8"
979
+ },
980
+ "language_model.model.layers.6.self_attn.v_proj": {
981
+ "group_size": 32,
982
+ "bits": 8,
983
+ "mode": "mxfp8"
984
+ },
985
+ "language_model.model.layers.6.self_attn.o_proj": {
986
+ "group_size": 32,
987
+ "bits": 8,
988
+ "mode": "mxfp8"
989
+ },
990
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj": {
991
+ "group_size": 32,
992
+ "bits": 8,
993
+ "mode": "mxfp8"
994
+ },
995
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj": {
996
+ "group_size": 32,
997
+ "bits": 8,
998
+ "mode": "mxfp8"
999
+ },
1000
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj": {
1001
+ "group_size": 32,
1002
+ "bits": 8,
1003
+ "mode": "mxfp8"
1004
+ },
1005
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj": {
1006
+ "group_size": 32,
1007
+ "bits": 8,
1008
+ "mode": "mxfp8"
1009
+ },
1010
+ "language_model.model.layers.6.mlp.shared_experts.up_proj": {
1011
+ "group_size": 32,
1012
+ "bits": 8,
1013
+ "mode": "mxfp8"
1014
+ },
1015
+ "language_model.model.layers.6.mlp.shared_experts.down_proj": {
1016
+ "group_size": 32,
1017
+ "bits": 8,
1018
+ "mode": "mxfp8"
1019
+ },
1020
+ "language_model.model.layers.7.self_attn.q_proj": {
1021
+ "group_size": 32,
1022
+ "bits": 8,
1023
+ "mode": "mxfp8"
1024
+ },
1025
+ "language_model.model.layers.7.self_attn.k_proj": {
1026
+ "group_size": 32,
1027
+ "bits": 8,
1028
+ "mode": "mxfp8"
1029
+ },
1030
+ "language_model.model.layers.7.self_attn.v_proj": {
1031
+ "group_size": 32,
1032
+ "bits": 8,
1033
+ "mode": "mxfp8"
1034
+ },
1035
+ "language_model.model.layers.7.self_attn.o_proj": {
1036
+ "group_size": 32,
1037
+ "bits": 8,
1038
+ "mode": "mxfp8"
1039
+ },
1040
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj": {
1041
+ "group_size": 32,
1042
+ "bits": 8,
1043
+ "mode": "mxfp8"
1044
+ },
1045
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj": {
1046
+ "group_size": 32,
1047
+ "bits": 8,
1048
+ "mode": "mxfp8"
1049
+ },
1050
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj": {
1051
+ "group_size": 32,
1052
+ "bits": 8,
1053
+ "mode": "mxfp8"
1054
+ },
1055
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj": {
1056
+ "group_size": 32,
1057
+ "bits": 8,
1058
+ "mode": "mxfp8"
1059
+ },
1060
+ "language_model.model.layers.7.mlp.shared_experts.up_proj": {
1061
+ "group_size": 32,
1062
+ "bits": 8,
1063
+ "mode": "mxfp8"
1064
+ },
1065
+ "language_model.model.layers.7.mlp.shared_experts.down_proj": {
1066
+ "group_size": 32,
1067
+ "bits": 8,
1068
+ "mode": "mxfp8"
1069
+ },
1070
+ "language_model.model.layers.8.self_attn.q_proj": {
1071
+ "group_size": 32,
1072
+ "bits": 8,
1073
+ "mode": "mxfp8"
1074
+ },
1075
+ "language_model.model.layers.8.self_attn.k_proj": {
1076
+ "group_size": 32,
1077
+ "bits": 8,
1078
+ "mode": "mxfp8"
1079
+ },
1080
+ "language_model.model.layers.8.self_attn.v_proj": {
1081
+ "group_size": 32,
1082
+ "bits": 8,
1083
+ "mode": "mxfp8"
1084
+ },
1085
+ "language_model.model.layers.8.self_attn.o_proj": {
1086
+ "group_size": 32,
1087
+ "bits": 8,
1088
+ "mode": "mxfp8"
1089
+ },
1090
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj": {
1091
+ "group_size": 32,
1092
+ "bits": 8,
1093
+ "mode": "mxfp8"
1094
+ },
1095
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj": {
1096
+ "group_size": 32,
1097
+ "bits": 8,
1098
+ "mode": "mxfp8"
1099
+ },
1100
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj": {
1101
+ "group_size": 32,
1102
+ "bits": 8,
1103
+ "mode": "mxfp8"
1104
+ },
1105
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj": {
1106
+ "group_size": 32,
1107
+ "bits": 8,
1108
+ "mode": "mxfp8"
1109
+ },
1110
+ "language_model.model.layers.8.mlp.shared_experts.up_proj": {
1111
+ "group_size": 32,
1112
+ "bits": 8,
1113
+ "mode": "mxfp8"
1114
+ },
1115
+ "language_model.model.layers.8.mlp.shared_experts.down_proj": {
1116
+ "group_size": 32,
1117
+ "bits": 8,
1118
+ "mode": "mxfp8"
1119
+ },
1120
+ "language_model.model.layers.9.self_attn.q_proj": {
1121
+ "group_size": 32,
1122
+ "bits": 8,
1123
+ "mode": "mxfp8"
1124
+ },
1125
+ "language_model.model.layers.9.self_attn.k_proj": {
1126
+ "group_size": 32,
1127
+ "bits": 8,
1128
+ "mode": "mxfp8"
1129
+ },
1130
+ "language_model.model.layers.9.self_attn.v_proj": {
1131
+ "group_size": 32,
1132
+ "bits": 8,
1133
+ "mode": "mxfp8"
1134
+ },
1135
+ "language_model.model.layers.9.self_attn.o_proj": {
1136
+ "group_size": 32,
1137
+ "bits": 8,
1138
+ "mode": "mxfp8"
1139
+ },
1140
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj": {
1141
+ "group_size": 32,
1142
+ "bits": 8,
1143
+ "mode": "mxfp8"
1144
+ },
1145
+ "language_model.model.layers.9.mlp.switch_mlp.up_proj": {
1146
+ "group_size": 32,
1147
+ "bits": 8,
1148
+ "mode": "mxfp8"
1149
+ },
1150
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj": {
1151
+ "group_size": 32,
1152
+ "bits": 8,
1153
+ "mode": "mxfp8"
1154
+ },
1155
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj": {
1156
+ "group_size": 32,
1157
+ "bits": 8,
1158
+ "mode": "mxfp8"
1159
+ },
1160
+ "language_model.model.layers.9.mlp.shared_experts.up_proj": {
1161
+ "group_size": 32,
1162
+ "bits": 8,
1163
+ "mode": "mxfp8"
1164
+ },
1165
+ "language_model.model.layers.9.mlp.shared_experts.down_proj": {
1166
+ "group_size": 32,
1167
+ "bits": 8,
1168
+ "mode": "mxfp8"
1169
+ },
1170
+ "language_model.model.layers.10.self_attn.q_proj": {
1171
+ "group_size": 32,
1172
+ "bits": 8,
1173
+ "mode": "mxfp8"
1174
+ },
1175
+ "language_model.model.layers.10.self_attn.k_proj": {
1176
+ "group_size": 32,
1177
+ "bits": 8,
1178
+ "mode": "mxfp8"
1179
+ },
1180
+ "language_model.model.layers.10.self_attn.v_proj": {
1181
+ "group_size": 32,
1182
+ "bits": 8,
1183
+ "mode": "mxfp8"
1184
+ },
1185
+ "language_model.model.layers.10.self_attn.o_proj": {
1186
+ "group_size": 32,
1187
+ "bits": 8,
1188
+ "mode": "mxfp8"
1189
+ },
1190
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj": {
1191
+ "group_size": 32,
1192
+ "bits": 8,
1193
+ "mode": "mxfp8"
1194
+ },
1195
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj": {
1196
+ "group_size": 32,
1197
+ "bits": 8,
1198
+ "mode": "mxfp8"
1199
+ },
1200
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj": {
1201
+ "group_size": 32,
1202
+ "bits": 8,
1203
+ "mode": "mxfp8"
1204
+ },
1205
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj": {
1206
+ "group_size": 32,
1207
+ "bits": 8,
1208
+ "mode": "mxfp8"
1209
+ },
1210
+ "language_model.model.layers.10.mlp.shared_experts.up_proj": {
1211
+ "group_size": 32,
1212
+ "bits": 8,
1213
+ "mode": "mxfp8"
1214
+ },
1215
+ "language_model.model.layers.10.mlp.shared_experts.down_proj": {
1216
+ "group_size": 32,
1217
+ "bits": 8,
1218
+ "mode": "mxfp8"
1219
+ },
1220
+ "language_model.model.layers.11.self_attn.q_proj": {
1221
+ "group_size": 32,
1222
+ "bits": 8,
1223
+ "mode": "mxfp8"
1224
+ },
1225
+ "language_model.model.layers.11.self_attn.k_proj": {
1226
+ "group_size": 32,
1227
+ "bits": 8,
1228
+ "mode": "mxfp8"
1229
+ },
1230
+ "language_model.model.layers.11.self_attn.v_proj": {
1231
+ "group_size": 32,
1232
+ "bits": 8,
1233
+ "mode": "mxfp8"
1234
+ },
1235
+ "language_model.model.layers.11.self_attn.o_proj": {
1236
+ "group_size": 32,
1237
+ "bits": 8,
1238
+ "mode": "mxfp8"
1239
+ },
1240
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj": {
1241
+ "group_size": 32,
1242
+ "bits": 8,
1243
+ "mode": "mxfp8"
1244
+ },
1245
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj": {
1246
+ "group_size": 32,
1247
+ "bits": 8,
1248
+ "mode": "mxfp8"
1249
+ },
1250
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj": {
1251
+ "group_size": 32,
1252
+ "bits": 8,
1253
+ "mode": "mxfp8"
1254
+ },
1255
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj": {
1256
+ "group_size": 32,
1257
+ "bits": 8,
1258
+ "mode": "mxfp8"
1259
+ },
1260
+ "language_model.model.layers.11.mlp.shared_experts.up_proj": {
1261
+ "group_size": 32,
1262
+ "bits": 8,
1263
+ "mode": "mxfp8"
1264
+ },
1265
+ "language_model.model.layers.11.mlp.shared_experts.down_proj": {
1266
+ "group_size": 32,
1267
+ "bits": 8,
1268
+ "mode": "mxfp8"
1269
+ },
1270
+ "language_model.lm_head": {
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,
1282
+ "sliding_window": 128,
1283
+ "sliding_window_size": 128,
1284
+ "tile_tag": "2D",
1285
+ "topk_group": 1,
1286
+ "topk_method": "greedy",
1287
+ "transformers_version": "4.46.3",
1288
+ "use_mla": false,
1289
+ "v_head_dim": 128,
1290
+ "vision_config": {
1291
+ "image_size": 1024,
1292
+ "mlp_ratio": 3.7362,
1293
+ "model_name": "deeplip_b_l",
1294
+ "model_type": "vision",
1295
+ "width": {
1296
+ "clip-l-14-224": {
1297
+ "heads": 16,
1298
+ "image_size": 224,
1299
+ "layers": 24,
1300
+ "patch_size": 14,
1301
+ "width": 1024
1302
+ },
1303
+ "sam_vit_b": {
1304
+ "downsample_channels": [
1305
+ 512,
1306
+ 1024
1307
+ ],
1308
+ "global_attn_indexes": [
1309
+ 2,
1310
+ 5,
1311
+ 8,
1312
+ 11
1313
+ ],
1314
+ "heads": 12,
1315
+ "layers": 12,
1316
+ "width": 768
1317
+ }
1318
+ }
1319
+ },
1320
+ "vocab_size": 129280
1321
+ }
docs/PRD.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Product Requirements Document: Verified OCR-Aware Unlimited-OCR for MLX
2
+
3
+ Status: Approved for implementation
4
+ Owner: AutomatosX
5
+ Target release: `AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8`
6
+
7
+ ## 1. Problem
8
+
9
+ The current project improves inference ergonomics around an existing third-party
10
+ MXFP8 checkpoint, but it does not yet prove that AutomatosX produced distinct,
11
+ OCR-aware model weights. The existing precision map has no measured sensitivity
12
+ overrides, the evaluation dataset is empty, and the legacy upload path can
13
+ republish source weights with only metadata changes.
14
+
15
+ Users need an Apple Silicon checkpoint whose model-level optimization is
16
+ reproducible, measurable, and clearly separated from application tooling.
17
+
18
+ ## 2. Product outcome
19
+
20
+ Produce and publish a distinct Unlimited-OCR MLX checkpoint converted from the
21
+ upstream Baidu BF16 model. Quantization decisions must be derived from OCR
22
+ sensitivity measurements, and publication must be blocked unless quality,
23
+ performance, provenance, and R-SWA checks pass.
24
+
25
+ ## 3. Users and jobs
26
+
27
+ - Apple Silicon developers converting PDFs and images to text or Markdown.
28
+ - RAG teams requiring local document ingestion with predictable memory use.
29
+ - Model engineers who need reproducible quantization and benchmark evidence.
30
+ - Reviewers who need to distinguish new weights from repackaged checkpoints.
31
+
32
+ ## 4. Goals
33
+
34
+ 1. Start from an unquantized BF16 `baidu/Unlimited-OCR` checkpoint.
35
+ 2. Measure BF16 baseline quality before quantization.
36
+ 3. Measure quantization sensitivity for each supported module group.
37
+ 4. Keep sensitive groups in BF16, quantize eligible groups to MXFP8, and allow
38
+ a measured 8-bit affine exception when it is the only configuration that
39
+ satisfies both quality and throughput gates.
40
+ 5. Produce a candidate checkpoint with a deterministic provenance manifest.
41
+ 6. Compare the candidate with both BF16 and the Sahil MXFP8 reference.
42
+ 7. Verify native Unlimited-OCR R-SWA behavior with an 8K-or-longer run.
43
+ 8. Publish atomically to the AutomatosX namespace only after all gates pass.
44
+
45
+ ## 5. Non-goals
46
+
47
+ - Retraining the Baidu base model or claiming a new foundation model.
48
+ - LoRA fine-tuning for a specific document domain.
49
+ - Claiming upstream paper scores without reproducing them.
50
+ - Publishing an unvalidated checkpoint merely because conversion completed.
51
+ - Implementing true multi-image one-shot PDF inference in this release.
52
+
53
+ ## 6. Functional requirements
54
+
55
+ ### FR-1 Source verification
56
+
57
+ - Reject an already quantized model as the sensitivity or conversion source.
58
+ - Record source repository/path, revision when available, config digest, and
59
+ weight-file SHA-256 values.
60
+
61
+ ### FR-2 Evaluation data
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.
68
+
69
+ ### FR-3 Sensitivity analysis
70
+
71
+ - Evaluate BF16 once, then quantize one supported group at a time.
72
+ - Record matched concrete module paths and CER, digit-CER, and table-score
73
+ deltas for every group.
74
+ - Fail if any requested group cannot be evaluated.
75
+
76
+ ### FR-4 Precision-map generation
77
+
78
+ - Generate executable top-level precision rules; metadata-only nested rules do
79
+ not count.
80
+ - Promote a group to BF16 if any configured quality threshold is exceeded.
81
+ - Preserve vision, projector, embeddings, normalisation, LM head, and MoE gate
82
+ in BF16 unless explicit evidence supports quantizing them.
83
+ - Select any exception through a recorded multi-candidate calibration; a manual
84
+ precision-map edit is not sufficient release evidence.
85
+
86
+ ### FR-5 Conversion
87
+
88
+ - Convert from BF16 in a staging directory.
89
+ - Fail if no module is quantized, a requested rule matches nothing, or no model
90
+ weights are produced.
91
+ - Write `precision_map.json` and `quantization_summary.json` beside the weights.
92
+ - Record every quantized module's actual mode; the released configuration uses
93
+ MXFP8 for 119 modules and affine INT8 for the LM head.
94
+
95
+ ### FR-6 Comparative validation
96
+
97
+ - Benchmark BF16, Sahil MXFP8, and the candidate with the same inputs, prompts,
98
+ token budgets, and software versions.
99
+ - Store per-sample results as well as aggregates.
100
+
101
+ ### FR-7 Release gates
102
+
103
+ Publication requires all of the following:
104
+
105
+ - Candidate weight SHA-256 differs from the Sahil reference.
106
+ - Candidate mean CER is no more than 0.01 absolute above BF16.
107
+ - Candidate mean CER is no worse than the Sahil reference by more than 0.005.
108
+ - Digit CER degradation versus BF16 is no more than 0.01 absolute.
109
+ - Table score degradation versus BF16 is no more than 0.01 absolute.
110
+ - Candidate decode throughput is at least 90% of the Sahil reference.
111
+ - Candidate model weights are no larger than 4.5 GB.
112
+ - R-SWA validation reaches at least 8192 generated tokens with bounded-cache
113
+ evidence, or explicitly reports an inconclusive gate and blocks release.
114
+ - Every required artifact and digest is present.
115
+
116
+ ### FR-8 Publication
117
+
118
+ - Create or update `AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8` atomically.
119
+ - Upload only the validated candidate weights, metadata, model card, license,
120
+ inference package, benchmark summaries, and release manifest.
121
+ - Never silently fall back to uploading Sahil weights.
122
+
123
+ ### FR-9 Claims
124
+
125
+ - The model card must distinguish upstream paper results, third-party reference
126
+ results, and AutomatosX measurements.
127
+ - Failed, skipped, estimated, and inconclusive measurements must be labelled.
128
+
129
+ ## 7. Success metrics
130
+
131
+ - A public Hugging Face model repository contains distinct candidate weights.
132
+ - `release_manifest.json` has `release_approved: true` and every gate result.
133
+ - A clean environment can reproduce conversion from the recorded source.
134
+ - The model loads through `mlx-vlm` using native `model_type=unlimited-ocr`.
135
+ - CLI single-image OCR succeeds with the published model ID.
136
+
137
+ ## 8. Risks
138
+
139
+ - Evaluation data may be too small or biased to identify fragile layers.
140
+ - Group-level sensitivity can hide individual-layer outliers.
141
+ - MXFP8 support or module paths may change between `mlx-vlm` versions.
142
+ - Long-output documents may terminate naturally before the R-SWA test length.
143
+ - Public benchmark licenses may restrict redistribution of evaluation images.
144
+
145
+ Mitigations include pinned versions, digest-based provenance, fail-closed gates,
146
+ per-sample results, and release notes that state dataset limitations.
147
+
148
+ ## 9. Rollout
149
+
150
+ 1. Development: unit tests and synthetic smoke data.
151
+ 2. Candidate: BF16 sensitivity, conversion, and private/local validation.
152
+ 3. Release: approve manifest, create public model repository, upload atomically.
153
+ 4. Post-release: load the exact remote revision and run a final OCR smoke test.
docs/TECHNICAL_SPEC.md ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Technical Specification: OCR-Aware Mixed-Precision Release Pipeline
2
+
3
+ Status: Implementation specification
4
+ Target repository: `AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8`
5
+
6
+ ## 1. System overview
7
+
8
+ ```text
9
+ BF16 Baidu checkpoint
10
+ |
11
+ v
12
+ source + dataset preflight
13
+ |
14
+ v
15
+ BF16 baseline evaluation
16
+ |
17
+ v
18
+ one-group-at-a-time MXFP8 sensitivity
19
+ |
20
+ v
21
+ joint quality/throughput head calibration
22
+ |
23
+ v
24
+ generated executable precision map
25
+ |
26
+ v
27
+ staged BF16 -> mixed MXFP8 conversion
28
+ |
29
+ v
30
+ candidate vs BF16 vs Sahil validation
31
+ |
32
+ v
33
+ accuracy + performance + R-SWA release gates
34
+ |
35
+ v
36
+ content-addressed release manifest
37
+ |
38
+ v
39
+ atomic Hugging Face publication
40
+ ```
41
+
42
+ ## 2. Components
43
+
44
+ ### 2.1 Dataset validator
45
+
46
+ Input layout:
47
+
48
+ ```text
49
+ eval_dir/
50
+ manifest.json
51
+ images/<sample>.<ext>
52
+ ground_truth/<sample>.txt
53
+ ```
54
+
55
+ The validator checks unique stems, complete pairs, non-empty ground truth,
56
+ manifest consistency, category counts, and a SHA-256 digest over the manifest
57
+ and referenced files. Validation occurs before importing MLX.
58
+
59
+ ### 2.2 Sensitivity runner
60
+
61
+ `quantization/layer_sensitivity.py` loads the unquantized model and computes one
62
+ BF16 baseline. For each module group it reloads BF16, quantizes only the group,
63
+ and records:
64
+
65
+ - matched concrete module paths;
66
+ - sample count and task coverage;
67
+ - CER, digit CER, and table score;
68
+ - deltas from BF16;
69
+ - elapsed time and status.
70
+
71
+ The runner rejects a source config containing `quantization` or
72
+ `quantization_config`.
73
+
74
+ ### 2.3 Precision-map generator
75
+
76
+ The generator reads `layer_groups` from the sensitivity result and the approved
77
+ calibration result. It starts from the conservative base map, promotes
78
+ sensitive or failed groups to BF16, and applies only calibration overrides that
79
+ were selected by existing release thresholds.
80
+ Generated rules are top-level executable entries. Metadata is stored under keys
81
+ beginning with `_`, which the converter ignores.
82
+
83
+ Rule precedence is:
84
+
85
+ 1. exact concrete module path;
86
+ 2. more-specific wildcard path;
87
+ 3. general wildcard path;
88
+ 4. unlisted modules default to BF16.
89
+
90
+ ### 2.4 Converter
91
+
92
+ `quantization/mixed_precision_convert.py` calls the pinned `mlx-vlm` converter
93
+ with per-module quantization settings. The main model uses MXFP8 with eight data
94
+ bits and group size 32; the calibrated LM head uses 8-bit affine quantization
95
+ with group size 32. It writes into a new sibling staging directory and
96
+ atomically renames it only after verifying:
97
+
98
+ - at least one module was quantized;
99
+ - every requested MXFP8 or affine8 rule matched;
100
+ - at least one Safetensors file exists;
101
+ - model metadata selects native Unlimited-OCR;
102
+ - `precision_map.json` and `quantization_summary.json` were written.
103
+
104
+ The requested final output directory must not exist.
105
+
106
+ ### 2.5 Comparative benchmark runner
107
+
108
+ For each model (`bf16`, `reference_mxfp8`, `candidate`), use identical dataset,
109
+ prompt, profile, max tokens, and preprocessing settings. Results are written to:
110
+
111
+ ```text
112
+ 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
119
+ candidate_accuracy.json
120
+ reference_performance.json
121
+ candidate_performance.json
122
+ candidate_rswa.json
123
+ release_manifest.json
124
+ ```
125
+
126
+ ### 2.6 Release gate
127
+
128
+ The release gate is a pure-data operation. It reads benchmark JSON and candidate
129
+ artifacts without loading a model. Every gate records:
130
+
131
+ ```json
132
+ {
133
+ "name": "candidate_cer_vs_bf16",
134
+ "passed": true,
135
+ "actual": 0.012,
136
+ "limit": 0.01,
137
+ "detail": "candidate - bf16 absolute CER"
138
+ }
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
+
146
+ The publisher accepts only a candidate directory containing:
147
+
148
+ - model Safetensors and valid index when sharded;
149
+ - tokenizer and processor metadata;
150
+ - generated precision map;
151
+ - quantization summary;
152
+ - approved release manifest;
153
+ - README/model card and license.
154
+
155
+ Before upload it recomputes candidate hashes and compares them with the approved
156
+ manifest. It then creates one Hugging Face commit. Source checkpoint download or
157
+ fallback is forbidden in this path.
158
+
159
+ ## 3. CLI
160
+
161
+ Primary orchestration command:
162
+
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
171
+ ```
172
+
173
+ Publication command:
174
+
175
+ ```bash
176
+ python scripts/publish_optimized_model.py \
177
+ --model-dir models/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
178
+ --manifest artifacts/ocr-aware-v1/release_manifest.json \
179
+ --artifacts-dir artifacts/ocr-aware-v1 \
180
+ --repo-id AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8
181
+ ```
182
+
183
+ 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;
204
+ - Apple chip and unified memory;
205
+ - generation parameters and artifact digests;
206
+ - generated precision-map digest.
207
+
208
+ ## 5. Model metadata
209
+
210
+ Published `config.json` must have:
211
+
212
+ - architecture `UnlimitedOCRForCausalLM`;
213
+ - `model_type: unlimited-ocr`;
214
+ - a positive sliding-window size;
215
+ - an MXFP8 quantization object generated by the converter.
216
+ - per-module entries showing 119 MXFP8 modules and one affine8 LM head.
217
+
218
+ Published `processor_config.json` must use:
219
+
220
+ - `processor_class: UnlimitedOCRHFProcessor`;
221
+ - `sft_format: unlimitedocr`.
222
+
223
+ ## 6. Error handling
224
+
225
+ - Never delete or overwrite an existing output model directory.
226
+ - Each subprocess failure stops the pipeline.
227
+ - Missing metrics, zero samples, unmatched rules, natural short generations,
228
+ and unavailable Metal are explicit failures or inconclusive release blockers.
229
+ - Partial candidate output remains only in a named staging directory until the
230
+ converter cleans it up.
231
+ - Publication recomputes hashes immediately before creating the remote commit.
232
+
233
+ ## 7. Testing
234
+
235
+ Unit tests cover:
236
+
237
+ - source rejection for already quantized models;
238
+ - sensitivity-result schema parsing;
239
+ - objective calibration selection across BF16, MXFP8, and affine8 heads;
240
+ - sensitive-group promotion to executable BF16 rules;
241
+ - specificity precedence in the precision map;
242
+ - output-directory lifecycle;
243
+ - release-gate pass, fail, missing, and inconclusive cases;
244
+ - hash mismatch rejection;
245
+ - publisher refusal without an approved manifest;
246
+ - publisher operation list containing candidate rather than source weights.
247
+
248
+ Integration tests mock model conversion and Hugging Face APIs. Hardware smoke
249
+ tests run BF16 and candidate inference on a real image. Full release validation
250
+ runs only on a Metal-capable Apple Silicon host.
251
+
252
+ ## 8. Release verification
253
+
254
+ After publication:
255
+
256
+ 1. Read remote file metadata and confirm all expected files exist.
257
+ 2. Compare remote LFS SHA-256 with the approved manifest.
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.
docs/adr/0001-verified-ocr-aware-quantization.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ADR-0001: Build a Verified Checkpoint from BF16 and Publish Fail-Closed
2
+
3
+ Status: Accepted
4
+ Date: 2026-07-20
5
+
6
+ ## Context
7
+
8
+ The project currently references `sahilchachra/unlimited-ocr-mxfp8-mlx` as its
9
+ weight source. That checkpoint is already MXFP8. Re-uploading it with different
10
+ configuration or pipeline files improves runtime behavior but is not a new
11
+ weight optimization.
12
+
13
+ The existing repository also contains sensitivity and mixed-precision scripts,
14
+ but measured overrides have not been generated and the legacy upload workflow
15
+ does not require proof that candidate weights differ from the source.
16
+
17
+ ## Decision
18
+
19
+ We will:
20
+
21
+ 1. Use the upstream Baidu BF16 checkpoint as the only valid source for
22
+ sensitivity analysis and final conversion.
23
+ 2. Use the Sahil MXFP8 checkpoint only as a comparative reference.
24
+ 3. Generate the precision map from measured group-level OCR deltas.
25
+ 4. Preserve high-risk optical and routing components in BF16 by default.
26
+ 5. Quantize eligible language-tower linear modules to block-float MXFP8 with
27
+ group size 32.
28
+ 6. Select the native `unlimited-ocr` MLX backend so R-SWA uses its bounded
29
+ ring/sliding cache rather than a legacy DeepSeek-OCR fallback.
30
+ 7. Generate a signed-by-content release manifest containing hashes, versions,
31
+ metrics, and explicit pass/fail gates.
32
+ 8. Make publication fail-closed: no approved manifest means no upload.
33
+ 9. Publish under a new, provenance-clear repository:
34
+ `AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8`.
35
+
36
+ ## Sensitivity policy
37
+
38
+ A module group is retained in BF16 when any of these conditions is true:
39
+
40
+ - CER delta is greater than 0.02 absolute.
41
+ - Digit-CER delta is greater than 0.02 absolute.
42
+ - Table-score degradation is greater than 0.01 absolute.
43
+ - The group could not be measured reliably.
44
+ - It belongs to the protected optical/routing set.
45
+
46
+ Concrete exact-path overrides take precedence over wildcard MXFP8 rules.
47
+
48
+ ## Alternatives considered
49
+
50
+ ### Repackage the Sahil checkpoint
51
+
52
+ Rejected as the primary release path. It is useful for runtime tooling, but its
53
+ weights are not an AutomatosX optimization.
54
+
55
+ ### Re-quantize the Sahil checkpoint
56
+
57
+ Rejected. Quantizing already quantized weights compounds error and prevents a
58
+ clean BF16 comparison.
59
+
60
+ ### Uniform MXFP8 from BF16
61
+
62
+ Rejected as the final product because it does not use OCR sensitivity evidence.
63
+ It remains a useful control candidate.
64
+
65
+ ### Per-layer exhaustive search
66
+
67
+ Deferred. It provides finer control but multiplies evaluation cost. Group-level
68
+ analysis is the first release; the data contract keeps concrete module paths so
69
+ a later release can promote individual layers.
70
+
71
+ ### Publish first and benchmark later
72
+
73
+ Rejected. It would recreate the current ambiguity and allow unsupported claims.
74
+
75
+ ## Consequences
76
+
77
+ Positive:
78
+
79
+ - The published checkpoint is demonstrably distinct and reproducible.
80
+ - Quality claims are tied to recorded evidence.
81
+ - Runtime improvements and weight improvements are clearly separated.
82
+ - A broken or incomplete benchmark cannot accidentally publish a model.
83
+
84
+ Negative:
85
+
86
+ - Release requires a representative evaluation dataset and Metal-capable Mac.
87
+ - Sensitivity analysis is compute intensive.
88
+ - Conservative BF16 retention may produce a larger model than uniform MXFP8.
89
+ - A failed R-SWA or accuracy gate intentionally delays publication.
90
+
docs/adr/0002-calibrated-affine8-lm-head.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ADR-0002: Use Affine INT8 for the LM Head
2
+
3
+ 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
10
+ the projector and token embedding could be quantized without degrading CER,
11
+ digit CER, or table score. Joint validation then exposed a trade-off isolated
12
+ group sensitivity could not predict:
13
+
14
+ | LM-head mode | Mean CER | Digit CER | Decode tok/s | Release result |
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
22
+ pre-existing quality and 90% throughput limits; thresholds were not changed
23
+ after observing the results.
24
+
25
+ ## Decision
26
+
27
+ Use MXFP8, group size 32, for 119 eligible modules. Use 8-bit affine
28
+ quantization, group size 32, for `language_model.lm_head`. Keep the vision
29
+ encoder and non-quantizable normalization/routing state in BF16.
30
+
31
+ `quantization/calibrate_precision.py` selects the fastest candidate that passes
32
+ every existing release constraint and records the decision in
33
+ `calibration_results.json`. The generated precision map consumes that record,
34
+ so the affine exception is reproducible and is not a manual release edit.
35
+
36
+ ## Alternatives considered
37
+
38
+ ### Keep the LM head in BF16
39
+
40
+ Rejected for this release because measured throughput was 86.7% of the Sahil
41
+ reference, below the approved 90% floor.
42
+
43
+ ### Quantize the LM head to MXFP8
44
+
45
+ Rejected because mean CER degraded by 0.02098 absolute versus BF16, above the
46
+ 0.01 ceiling, even though throughput passed.
47
+
48
+ ### Lower the throughput or CER threshold
49
+
50
+ Rejected. Changing a gate after seeing a failing candidate would invalidate the
51
+ fail-closed release design.
52
+
53
+ ## Consequences
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.
examples/batch_directory.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example: Batch OCR processing of a directory of images."""
2
+
3
+ from unlimited_ocr import OCRPipeline
4
+
5
+ # Initialize the pipeline
6
+ pipeline = OCRPipeline(
7
+ model_path="AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
8
+ verbose=True,
9
+ )
10
+
11
+ # --- Process all images in a directory ---
12
+ # Supports: .jpg, .jpeg, .png, .tiff, .tif, .webp, .bmp
13
+ results = pipeline.run_batch(
14
+ "./scanned_documents/",
15
+ format="text",
16
+ show_progress=True, # Rich progress bar
17
+ )
18
+
19
+ # Results is a dict: {filename: ocr_text}
20
+ for filename, text in results.items():
21
+ print(f"\n{'='*60}")
22
+ print(f"FILE: {filename}")
23
+ print(f"{'='*60}")
24
+ print(text[:500]) # Print first 500 chars
25
+
26
+ # --- Save each result to an output directory ---
27
+ results = pipeline.run_batch(
28
+ "./scanned_documents/",
29
+ format="markdown",
30
+ output_dir="./ocr_results/", # Creates .md files per image
31
+ show_progress=True,
32
+ )
33
+ print(f"\nProcessed {len(results)} files → ./ocr_results/")
34
+
35
+ # --- With preprocessing for low-quality scans ---
36
+ results = pipeline.run_batch(
37
+ "./low_quality_scans/",
38
+ format="text",
39
+ preprocess=True, # deskew + contrast enhancement
40
+ output_dir="./cleaned_results/",
41
+ )
42
+
43
+ # --- JSON output with bounding boxes ---
44
+ results = pipeline.run_batch(
45
+ "./forms/",
46
+ format="json",
47
+ grounding=True, # Include bounding box coordinates
48
+ output_dir="./structured_results/",
49
+ )
50
+
51
+ # Clean up
52
+ pipeline.cleanup()
examples/multi_page_pdf.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example: OCR a multi-page PDF document."""
2
+
3
+ from unlimited_ocr import OCRPipeline
4
+
5
+ # Initialize the pipeline
6
+ pipeline = OCRPipeline(
7
+ model_path="AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
8
+ verbose=True,
9
+ )
10
+
11
+ # --- Basic PDF OCR (all pages) ---
12
+ result = pipeline.run("your_document.pdf", format="text", dpi=300)
13
+ print(result)
14
+
15
+ # --- Save as Markdown with page headings ---
16
+ result = pipeline.run(
17
+ "your_document.pdf",
18
+ format="markdown",
19
+ dpi=300,
20
+ output_path="output.md",
21
+ )
22
+ print("Saved to output.md")
23
+
24
+ # --- JSON output with per-page structure ---
25
+ result = pipeline.run(
26
+ "your_document.pdf",
27
+ format="json",
28
+ dpi=300,
29
+ output_path="output.json",
30
+ )
31
+ print("Saved to output.json")
32
+
33
+ # --- With preprocessing for scanned PDFs ---
34
+ result = pipeline.run(
35
+ "scanned_document.pdf",
36
+ format="text",
37
+ preprocess=True, # deskew + contrast enhancement per page
38
+ dpi=300,
39
+ )
40
+ print(result)
41
+
42
+ # --- With progress tracking ---
43
+ def on_progress(current, total):
44
+ print(f" Processing page {current}/{total}...")
45
+
46
+ result = pipeline.run(
47
+ "long_document.pdf",
48
+ format="text",
49
+ progress_callback=on_progress,
50
+ )
51
+
52
+ # Clean up
53
+ pipeline.cleanup()
examples/single_image.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example: OCR a single image file."""
2
+
3
+ from unlimited_ocr import OCRPipeline
4
+
5
+ # Initialize the pipeline (model loads lazily on first inference)
6
+ pipeline = OCRPipeline(
7
+ model_path="AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
8
+ verbose=True,
9
+ )
10
+
11
+ # --- Basic document OCR ---
12
+ result = pipeline.run("your_document.jpg", format="text")
13
+ 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)
21
+ print(result)
22
+
23
+ # --- With image preprocessing (deskew + contrast enhancement) ---
24
+ result = pipeline.run("scanned_page.png", format="text", preprocess=True)
25
+ print(result)
26
+
27
+ # --- Different task types ---
28
+ # "document" — general document parsing (default)
29
+ # "markdown" — convert to markdown structure
30
+ # "figure" — parse figures/diagrams
31
+ # "free" — free-form OCR
32
+ result = pipeline.run("table.png", task="markdown", format="markdown")
33
+ print(result)
34
+
35
+ # Clean up temporary files
36
+ pipeline.cleanup()
generation_config.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_sample": false,
3
+ "temperature": 0.0,
4
+ "top_p": 1.0,
5
+ "max_new_tokens": 8192,
6
+ "repetition_penalty": 1.0,
7
+ "no_repeat_ngram_size": 35,
8
+ "use_cache": true,
9
+ "_comment": "Tested default generation parameters for Unlimited-OCR MLX. Deterministic decoding recommended for OCR accuracy.",
10
+ "_profiles": {
11
+ "accurate": {"max_new_tokens": 8192, "temperature": 0.0, "no_repeat_ngram_size": 35},
12
+ "fast": {"max_new_tokens": 2048, "temperature": 0.0, "no_repeat_ngram_size": 35},
13
+ "long-document": {"max_new_tokens": 16384, "temperature": 0.0, "repetition_penalty": 1.05, "no_repeat_ngram_size": 25},
14
+ "plain-text": {"max_new_tokens": 4096, "temperature": 0.0, "no_repeat_ngram_size": 35},
15
+ "markdown": {"max_new_tokens": 8192, "temperature": 0.0, "no_repeat_ngram_size": 35}
16
+ }
17
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27568517565832e42daa563f6939e66a8ebb50cce0b01a24ecebb3ea6b4d0b10
3
+ size 3843160559
model.safetensors.index.json ADDED
@@ -0,0 +1,759 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "metadata": {
3
+ "total_size": 3843066880
4
+ },
5
+ "weight_map": {
6
+ "image_newline": "model.safetensors",
7
+ "language_model.lm_head.biases": "model.safetensors",
8
+ "language_model.lm_head.scales": "model.safetensors",
9
+ "language_model.lm_head.weight": "model.safetensors",
10
+ "language_model.model.embed_tokens.scales": "model.safetensors",
11
+ "language_model.model.embed_tokens.weight": "model.safetensors",
12
+ "language_model.model.layers.0.input_layernorm.weight": "model.safetensors",
13
+ "language_model.model.layers.0.mlp.down_proj.scales": "model.safetensors",
14
+ "language_model.model.layers.0.mlp.down_proj.weight": "model.safetensors",
15
+ "language_model.model.layers.0.mlp.gate_proj.scales": "model.safetensors",
16
+ "language_model.model.layers.0.mlp.gate_proj.weight": "model.safetensors",
17
+ "language_model.model.layers.0.mlp.up_proj.scales": "model.safetensors",
18
+ "language_model.model.layers.0.mlp.up_proj.weight": "model.safetensors",
19
+ "language_model.model.layers.0.post_attention_layernorm.weight": "model.safetensors",
20
+ "language_model.model.layers.0.self_attn.k_proj.scales": "model.safetensors",
21
+ "language_model.model.layers.0.self_attn.k_proj.weight": "model.safetensors",
22
+ "language_model.model.layers.0.self_attn.o_proj.scales": "model.safetensors",
23
+ "language_model.model.layers.0.self_attn.o_proj.weight": "model.safetensors",
24
+ "language_model.model.layers.0.self_attn.q_proj.scales": "model.safetensors",
25
+ "language_model.model.layers.0.self_attn.q_proj.weight": "model.safetensors",
26
+ "language_model.model.layers.0.self_attn.v_proj.scales": "model.safetensors",
27
+ "language_model.model.layers.0.self_attn.v_proj.weight": "model.safetensors",
28
+ "language_model.model.layers.1.input_layernorm.weight": "model.safetensors",
29
+ "language_model.model.layers.1.mlp.gate.weight": "model.safetensors",
30
+ "language_model.model.layers.1.mlp.shared_experts.down_proj.scales": "model.safetensors",
31
+ "language_model.model.layers.1.mlp.shared_experts.down_proj.weight": "model.safetensors",
32
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj.scales": "model.safetensors",
33
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj.weight": "model.safetensors",
34
+ "language_model.model.layers.1.mlp.shared_experts.up_proj.scales": "model.safetensors",
35
+ "language_model.model.layers.1.mlp.shared_experts.up_proj.weight": "model.safetensors",
36
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj.scales": "model.safetensors",
37
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj.weight": "model.safetensors",
38
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
39
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
40
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj.scales": "model.safetensors",
41
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj.weight": "model.safetensors",
42
+ "language_model.model.layers.1.post_attention_layernorm.weight": "model.safetensors",
43
+ "language_model.model.layers.1.self_attn.k_proj.scales": "model.safetensors",
44
+ "language_model.model.layers.1.self_attn.k_proj.weight": "model.safetensors",
45
+ "language_model.model.layers.1.self_attn.o_proj.scales": "model.safetensors",
46
+ "language_model.model.layers.1.self_attn.o_proj.weight": "model.safetensors",
47
+ "language_model.model.layers.1.self_attn.q_proj.scales": "model.safetensors",
48
+ "language_model.model.layers.1.self_attn.q_proj.weight": "model.safetensors",
49
+ "language_model.model.layers.1.self_attn.v_proj.scales": "model.safetensors",
50
+ "language_model.model.layers.1.self_attn.v_proj.weight": "model.safetensors",
51
+ "language_model.model.layers.10.input_layernorm.weight": "model.safetensors",
52
+ "language_model.model.layers.10.mlp.gate.weight": "model.safetensors",
53
+ "language_model.model.layers.10.mlp.shared_experts.down_proj.scales": "model.safetensors",
54
+ "language_model.model.layers.10.mlp.shared_experts.down_proj.weight": "model.safetensors",
55
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj.scales": "model.safetensors",
56
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj.weight": "model.safetensors",
57
+ "language_model.model.layers.10.mlp.shared_experts.up_proj.scales": "model.safetensors",
58
+ "language_model.model.layers.10.mlp.shared_experts.up_proj.weight": "model.safetensors",
59
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj.scales": "model.safetensors",
60
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj.weight": "model.safetensors",
61
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
62
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
63
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj.scales": "model.safetensors",
64
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj.weight": "model.safetensors",
65
+ "language_model.model.layers.10.post_attention_layernorm.weight": "model.safetensors",
66
+ "language_model.model.layers.10.self_attn.k_proj.scales": "model.safetensors",
67
+ "language_model.model.layers.10.self_attn.k_proj.weight": "model.safetensors",
68
+ "language_model.model.layers.10.self_attn.o_proj.scales": "model.safetensors",
69
+ "language_model.model.layers.10.self_attn.o_proj.weight": "model.safetensors",
70
+ "language_model.model.layers.10.self_attn.q_proj.scales": "model.safetensors",
71
+ "language_model.model.layers.10.self_attn.q_proj.weight": "model.safetensors",
72
+ "language_model.model.layers.10.self_attn.v_proj.scales": "model.safetensors",
73
+ "language_model.model.layers.10.self_attn.v_proj.weight": "model.safetensors",
74
+ "language_model.model.layers.11.input_layernorm.weight": "model.safetensors",
75
+ "language_model.model.layers.11.mlp.gate.weight": "model.safetensors",
76
+ "language_model.model.layers.11.mlp.shared_experts.down_proj.scales": "model.safetensors",
77
+ "language_model.model.layers.11.mlp.shared_experts.down_proj.weight": "model.safetensors",
78
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj.scales": "model.safetensors",
79
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj.weight": "model.safetensors",
80
+ "language_model.model.layers.11.mlp.shared_experts.up_proj.scales": "model.safetensors",
81
+ "language_model.model.layers.11.mlp.shared_experts.up_proj.weight": "model.safetensors",
82
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj.scales": "model.safetensors",
83
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj.weight": "model.safetensors",
84
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
85
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
86
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj.scales": "model.safetensors",
87
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj.weight": "model.safetensors",
88
+ "language_model.model.layers.11.post_attention_layernorm.weight": "model.safetensors",
89
+ "language_model.model.layers.11.self_attn.k_proj.scales": "model.safetensors",
90
+ "language_model.model.layers.11.self_attn.k_proj.weight": "model.safetensors",
91
+ "language_model.model.layers.11.self_attn.o_proj.scales": "model.safetensors",
92
+ "language_model.model.layers.11.self_attn.o_proj.weight": "model.safetensors",
93
+ "language_model.model.layers.11.self_attn.q_proj.scales": "model.safetensors",
94
+ "language_model.model.layers.11.self_attn.q_proj.weight": "model.safetensors",
95
+ "language_model.model.layers.11.self_attn.v_proj.scales": "model.safetensors",
96
+ "language_model.model.layers.11.self_attn.v_proj.weight": "model.safetensors",
97
+ "language_model.model.layers.2.input_layernorm.weight": "model.safetensors",
98
+ "language_model.model.layers.2.mlp.gate.weight": "model.safetensors",
99
+ "language_model.model.layers.2.mlp.shared_experts.down_proj.scales": "model.safetensors",
100
+ "language_model.model.layers.2.mlp.shared_experts.down_proj.weight": "model.safetensors",
101
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj.scales": "model.safetensors",
102
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj.weight": "model.safetensors",
103
+ "language_model.model.layers.2.mlp.shared_experts.up_proj.scales": "model.safetensors",
104
+ "language_model.model.layers.2.mlp.shared_experts.up_proj.weight": "model.safetensors",
105
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj.scales": "model.safetensors",
106
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj.weight": "model.safetensors",
107
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
108
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
109
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj.scales": "model.safetensors",
110
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj.weight": "model.safetensors",
111
+ "language_model.model.layers.2.post_attention_layernorm.weight": "model.safetensors",
112
+ "language_model.model.layers.2.self_attn.k_proj.scales": "model.safetensors",
113
+ "language_model.model.layers.2.self_attn.k_proj.weight": "model.safetensors",
114
+ "language_model.model.layers.2.self_attn.o_proj.scales": "model.safetensors",
115
+ "language_model.model.layers.2.self_attn.o_proj.weight": "model.safetensors",
116
+ "language_model.model.layers.2.self_attn.q_proj.scales": "model.safetensors",
117
+ "language_model.model.layers.2.self_attn.q_proj.weight": "model.safetensors",
118
+ "language_model.model.layers.2.self_attn.v_proj.scales": "model.safetensors",
119
+ "language_model.model.layers.2.self_attn.v_proj.weight": "model.safetensors",
120
+ "language_model.model.layers.3.input_layernorm.weight": "model.safetensors",
121
+ "language_model.model.layers.3.mlp.gate.weight": "model.safetensors",
122
+ "language_model.model.layers.3.mlp.shared_experts.down_proj.scales": "model.safetensors",
123
+ "language_model.model.layers.3.mlp.shared_experts.down_proj.weight": "model.safetensors",
124
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj.scales": "model.safetensors",
125
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj.weight": "model.safetensors",
126
+ "language_model.model.layers.3.mlp.shared_experts.up_proj.scales": "model.safetensors",
127
+ "language_model.model.layers.3.mlp.shared_experts.up_proj.weight": "model.safetensors",
128
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj.scales": "model.safetensors",
129
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj.weight": "model.safetensors",
130
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
131
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
132
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj.scales": "model.safetensors",
133
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj.weight": "model.safetensors",
134
+ "language_model.model.layers.3.post_attention_layernorm.weight": "model.safetensors",
135
+ "language_model.model.layers.3.self_attn.k_proj.scales": "model.safetensors",
136
+ "language_model.model.layers.3.self_attn.k_proj.weight": "model.safetensors",
137
+ "language_model.model.layers.3.self_attn.o_proj.scales": "model.safetensors",
138
+ "language_model.model.layers.3.self_attn.o_proj.weight": "model.safetensors",
139
+ "language_model.model.layers.3.self_attn.q_proj.scales": "model.safetensors",
140
+ "language_model.model.layers.3.self_attn.q_proj.weight": "model.safetensors",
141
+ "language_model.model.layers.3.self_attn.v_proj.scales": "model.safetensors",
142
+ "language_model.model.layers.3.self_attn.v_proj.weight": "model.safetensors",
143
+ "language_model.model.layers.4.input_layernorm.weight": "model.safetensors",
144
+ "language_model.model.layers.4.mlp.gate.weight": "model.safetensors",
145
+ "language_model.model.layers.4.mlp.shared_experts.down_proj.scales": "model.safetensors",
146
+ "language_model.model.layers.4.mlp.shared_experts.down_proj.weight": "model.safetensors",
147
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj.scales": "model.safetensors",
148
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj.weight": "model.safetensors",
149
+ "language_model.model.layers.4.mlp.shared_experts.up_proj.scales": "model.safetensors",
150
+ "language_model.model.layers.4.mlp.shared_experts.up_proj.weight": "model.safetensors",
151
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj.scales": "model.safetensors",
152
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj.weight": "model.safetensors",
153
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
154
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
155
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj.scales": "model.safetensors",
156
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj.weight": "model.safetensors",
157
+ "language_model.model.layers.4.post_attention_layernorm.weight": "model.safetensors",
158
+ "language_model.model.layers.4.self_attn.k_proj.scales": "model.safetensors",
159
+ "language_model.model.layers.4.self_attn.k_proj.weight": "model.safetensors",
160
+ "language_model.model.layers.4.self_attn.o_proj.scales": "model.safetensors",
161
+ "language_model.model.layers.4.self_attn.o_proj.weight": "model.safetensors",
162
+ "language_model.model.layers.4.self_attn.q_proj.scales": "model.safetensors",
163
+ "language_model.model.layers.4.self_attn.q_proj.weight": "model.safetensors",
164
+ "language_model.model.layers.4.self_attn.v_proj.scales": "model.safetensors",
165
+ "language_model.model.layers.4.self_attn.v_proj.weight": "model.safetensors",
166
+ "language_model.model.layers.5.input_layernorm.weight": "model.safetensors",
167
+ "language_model.model.layers.5.mlp.gate.weight": "model.safetensors",
168
+ "language_model.model.layers.5.mlp.shared_experts.down_proj.scales": "model.safetensors",
169
+ "language_model.model.layers.5.mlp.shared_experts.down_proj.weight": "model.safetensors",
170
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj.scales": "model.safetensors",
171
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj.weight": "model.safetensors",
172
+ "language_model.model.layers.5.mlp.shared_experts.up_proj.scales": "model.safetensors",
173
+ "language_model.model.layers.5.mlp.shared_experts.up_proj.weight": "model.safetensors",
174
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj.scales": "model.safetensors",
175
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj.weight": "model.safetensors",
176
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
177
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
178
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj.scales": "model.safetensors",
179
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj.weight": "model.safetensors",
180
+ "language_model.model.layers.5.post_attention_layernorm.weight": "model.safetensors",
181
+ "language_model.model.layers.5.self_attn.k_proj.scales": "model.safetensors",
182
+ "language_model.model.layers.5.self_attn.k_proj.weight": "model.safetensors",
183
+ "language_model.model.layers.5.self_attn.o_proj.scales": "model.safetensors",
184
+ "language_model.model.layers.5.self_attn.o_proj.weight": "model.safetensors",
185
+ "language_model.model.layers.5.self_attn.q_proj.scales": "model.safetensors",
186
+ "language_model.model.layers.5.self_attn.q_proj.weight": "model.safetensors",
187
+ "language_model.model.layers.5.self_attn.v_proj.scales": "model.safetensors",
188
+ "language_model.model.layers.5.self_attn.v_proj.weight": "model.safetensors",
189
+ "language_model.model.layers.6.input_layernorm.weight": "model.safetensors",
190
+ "language_model.model.layers.6.mlp.gate.weight": "model.safetensors",
191
+ "language_model.model.layers.6.mlp.shared_experts.down_proj.scales": "model.safetensors",
192
+ "language_model.model.layers.6.mlp.shared_experts.down_proj.weight": "model.safetensors",
193
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj.scales": "model.safetensors",
194
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj.weight": "model.safetensors",
195
+ "language_model.model.layers.6.mlp.shared_experts.up_proj.scales": "model.safetensors",
196
+ "language_model.model.layers.6.mlp.shared_experts.up_proj.weight": "model.safetensors",
197
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj.scales": "model.safetensors",
198
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj.weight": "model.safetensors",
199
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
200
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
201
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj.scales": "model.safetensors",
202
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj.weight": "model.safetensors",
203
+ "language_model.model.layers.6.post_attention_layernorm.weight": "model.safetensors",
204
+ "language_model.model.layers.6.self_attn.k_proj.scales": "model.safetensors",
205
+ "language_model.model.layers.6.self_attn.k_proj.weight": "model.safetensors",
206
+ "language_model.model.layers.6.self_attn.o_proj.scales": "model.safetensors",
207
+ "language_model.model.layers.6.self_attn.o_proj.weight": "model.safetensors",
208
+ "language_model.model.layers.6.self_attn.q_proj.scales": "model.safetensors",
209
+ "language_model.model.layers.6.self_attn.q_proj.weight": "model.safetensors",
210
+ "language_model.model.layers.6.self_attn.v_proj.scales": "model.safetensors",
211
+ "language_model.model.layers.6.self_attn.v_proj.weight": "model.safetensors",
212
+ "language_model.model.layers.7.input_layernorm.weight": "model.safetensors",
213
+ "language_model.model.layers.7.mlp.gate.weight": "model.safetensors",
214
+ "language_model.model.layers.7.mlp.shared_experts.down_proj.scales": "model.safetensors",
215
+ "language_model.model.layers.7.mlp.shared_experts.down_proj.weight": "model.safetensors",
216
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj.scales": "model.safetensors",
217
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj.weight": "model.safetensors",
218
+ "language_model.model.layers.7.mlp.shared_experts.up_proj.scales": "model.safetensors",
219
+ "language_model.model.layers.7.mlp.shared_experts.up_proj.weight": "model.safetensors",
220
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj.scales": "model.safetensors",
221
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj.weight": "model.safetensors",
222
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
223
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
224
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj.scales": "model.safetensors",
225
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj.weight": "model.safetensors",
226
+ "language_model.model.layers.7.post_attention_layernorm.weight": "model.safetensors",
227
+ "language_model.model.layers.7.self_attn.k_proj.scales": "model.safetensors",
228
+ "language_model.model.layers.7.self_attn.k_proj.weight": "model.safetensors",
229
+ "language_model.model.layers.7.self_attn.o_proj.scales": "model.safetensors",
230
+ "language_model.model.layers.7.self_attn.o_proj.weight": "model.safetensors",
231
+ "language_model.model.layers.7.self_attn.q_proj.scales": "model.safetensors",
232
+ "language_model.model.layers.7.self_attn.q_proj.weight": "model.safetensors",
233
+ "language_model.model.layers.7.self_attn.v_proj.scales": "model.safetensors",
234
+ "language_model.model.layers.7.self_attn.v_proj.weight": "model.safetensors",
235
+ "language_model.model.layers.8.input_layernorm.weight": "model.safetensors",
236
+ "language_model.model.layers.8.mlp.gate.weight": "model.safetensors",
237
+ "language_model.model.layers.8.mlp.shared_experts.down_proj.scales": "model.safetensors",
238
+ "language_model.model.layers.8.mlp.shared_experts.down_proj.weight": "model.safetensors",
239
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj.scales": "model.safetensors",
240
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj.weight": "model.safetensors",
241
+ "language_model.model.layers.8.mlp.shared_experts.up_proj.scales": "model.safetensors",
242
+ "language_model.model.layers.8.mlp.shared_experts.up_proj.weight": "model.safetensors",
243
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj.scales": "model.safetensors",
244
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj.weight": "model.safetensors",
245
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
246
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
247
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj.scales": "model.safetensors",
248
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj.weight": "model.safetensors",
249
+ "language_model.model.layers.8.post_attention_layernorm.weight": "model.safetensors",
250
+ "language_model.model.layers.8.self_attn.k_proj.scales": "model.safetensors",
251
+ "language_model.model.layers.8.self_attn.k_proj.weight": "model.safetensors",
252
+ "language_model.model.layers.8.self_attn.o_proj.scales": "model.safetensors",
253
+ "language_model.model.layers.8.self_attn.o_proj.weight": "model.safetensors",
254
+ "language_model.model.layers.8.self_attn.q_proj.scales": "model.safetensors",
255
+ "language_model.model.layers.8.self_attn.q_proj.weight": "model.safetensors",
256
+ "language_model.model.layers.8.self_attn.v_proj.scales": "model.safetensors",
257
+ "language_model.model.layers.8.self_attn.v_proj.weight": "model.safetensors",
258
+ "language_model.model.layers.9.input_layernorm.weight": "model.safetensors",
259
+ "language_model.model.layers.9.mlp.gate.weight": "model.safetensors",
260
+ "language_model.model.layers.9.mlp.shared_experts.down_proj.scales": "model.safetensors",
261
+ "language_model.model.layers.9.mlp.shared_experts.down_proj.weight": "model.safetensors",
262
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj.scales": "model.safetensors",
263
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj.weight": "model.safetensors",
264
+ "language_model.model.layers.9.mlp.shared_experts.up_proj.scales": "model.safetensors",
265
+ "language_model.model.layers.9.mlp.shared_experts.up_proj.weight": "model.safetensors",
266
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj.scales": "model.safetensors",
267
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj.weight": "model.safetensors",
268
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj.scales": "model.safetensors",
269
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj.weight": "model.safetensors",
270
+ "language_model.model.layers.9.mlp.switch_mlp.up_proj.scales": "model.safetensors",
271
+ "language_model.model.layers.9.mlp.switch_mlp.up_proj.weight": "model.safetensors",
272
+ "language_model.model.layers.9.post_attention_layernorm.weight": "model.safetensors",
273
+ "language_model.model.layers.9.self_attn.k_proj.scales": "model.safetensors",
274
+ "language_model.model.layers.9.self_attn.k_proj.weight": "model.safetensors",
275
+ "language_model.model.layers.9.self_attn.o_proj.scales": "model.safetensors",
276
+ "language_model.model.layers.9.self_attn.o_proj.weight": "model.safetensors",
277
+ "language_model.model.layers.9.self_attn.q_proj.scales": "model.safetensors",
278
+ "language_model.model.layers.9.self_attn.q_proj.weight": "model.safetensors",
279
+ "language_model.model.layers.9.self_attn.v_proj.scales": "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.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",
287
+ "sam_model.blocks.0.attn.qkv.bias": "model.safetensors",
288
+ "sam_model.blocks.0.attn.qkv.weight": "model.safetensors",
289
+ "sam_model.blocks.0.attn.rel_pos_h": "model.safetensors",
290
+ "sam_model.blocks.0.attn.rel_pos_w": "model.safetensors",
291
+ "sam_model.blocks.0.mlp.lin1.bias": "model.safetensors",
292
+ "sam_model.blocks.0.mlp.lin1.weight": "model.safetensors",
293
+ "sam_model.blocks.0.mlp.lin2.bias": "model.safetensors",
294
+ "sam_model.blocks.0.mlp.lin2.weight": "model.safetensors",
295
+ "sam_model.blocks.0.norm1.bias": "model.safetensors",
296
+ "sam_model.blocks.0.norm1.weight": "model.safetensors",
297
+ "sam_model.blocks.0.norm2.bias": "model.safetensors",
298
+ "sam_model.blocks.0.norm2.weight": "model.safetensors",
299
+ "sam_model.blocks.1.attn.proj.bias": "model.safetensors",
300
+ "sam_model.blocks.1.attn.proj.weight": "model.safetensors",
301
+ "sam_model.blocks.1.attn.qkv.bias": "model.safetensors",
302
+ "sam_model.blocks.1.attn.qkv.weight": "model.safetensors",
303
+ "sam_model.blocks.1.attn.rel_pos_h": "model.safetensors",
304
+ "sam_model.blocks.1.attn.rel_pos_w": "model.safetensors",
305
+ "sam_model.blocks.1.mlp.lin1.bias": "model.safetensors",
306
+ "sam_model.blocks.1.mlp.lin1.weight": "model.safetensors",
307
+ "sam_model.blocks.1.mlp.lin2.bias": "model.safetensors",
308
+ "sam_model.blocks.1.mlp.lin2.weight": "model.safetensors",
309
+ "sam_model.blocks.1.norm1.bias": "model.safetensors",
310
+ "sam_model.blocks.1.norm1.weight": "model.safetensors",
311
+ "sam_model.blocks.1.norm2.bias": "model.safetensors",
312
+ "sam_model.blocks.1.norm2.weight": "model.safetensors",
313
+ "sam_model.blocks.10.attn.proj.bias": "model.safetensors",
314
+ "sam_model.blocks.10.attn.proj.weight": "model.safetensors",
315
+ "sam_model.blocks.10.attn.qkv.bias": "model.safetensors",
316
+ "sam_model.blocks.10.attn.qkv.weight": "model.safetensors",
317
+ "sam_model.blocks.10.attn.rel_pos_h": "model.safetensors",
318
+ "sam_model.blocks.10.attn.rel_pos_w": "model.safetensors",
319
+ "sam_model.blocks.10.mlp.lin1.bias": "model.safetensors",
320
+ "sam_model.blocks.10.mlp.lin1.weight": "model.safetensors",
321
+ "sam_model.blocks.10.mlp.lin2.bias": "model.safetensors",
322
+ "sam_model.blocks.10.mlp.lin2.weight": "model.safetensors",
323
+ "sam_model.blocks.10.norm1.bias": "model.safetensors",
324
+ "sam_model.blocks.10.norm1.weight": "model.safetensors",
325
+ "sam_model.blocks.10.norm2.bias": "model.safetensors",
326
+ "sam_model.blocks.10.norm2.weight": "model.safetensors",
327
+ "sam_model.blocks.11.attn.proj.bias": "model.safetensors",
328
+ "sam_model.blocks.11.attn.proj.weight": "model.safetensors",
329
+ "sam_model.blocks.11.attn.qkv.bias": "model.safetensors",
330
+ "sam_model.blocks.11.attn.qkv.weight": "model.safetensors",
331
+ "sam_model.blocks.11.attn.rel_pos_h": "model.safetensors",
332
+ "sam_model.blocks.11.attn.rel_pos_w": "model.safetensors",
333
+ "sam_model.blocks.11.mlp.lin1.bias": "model.safetensors",
334
+ "sam_model.blocks.11.mlp.lin1.weight": "model.safetensors",
335
+ "sam_model.blocks.11.mlp.lin2.bias": "model.safetensors",
336
+ "sam_model.blocks.11.mlp.lin2.weight": "model.safetensors",
337
+ "sam_model.blocks.11.norm1.bias": "model.safetensors",
338
+ "sam_model.blocks.11.norm1.weight": "model.safetensors",
339
+ "sam_model.blocks.11.norm2.bias": "model.safetensors",
340
+ "sam_model.blocks.11.norm2.weight": "model.safetensors",
341
+ "sam_model.blocks.2.attn.proj.bias": "model.safetensors",
342
+ "sam_model.blocks.2.attn.proj.weight": "model.safetensors",
343
+ "sam_model.blocks.2.attn.qkv.bias": "model.safetensors",
344
+ "sam_model.blocks.2.attn.qkv.weight": "model.safetensors",
345
+ "sam_model.blocks.2.attn.rel_pos_h": "model.safetensors",
346
+ "sam_model.blocks.2.attn.rel_pos_w": "model.safetensors",
347
+ "sam_model.blocks.2.mlp.lin1.bias": "model.safetensors",
348
+ "sam_model.blocks.2.mlp.lin1.weight": "model.safetensors",
349
+ "sam_model.blocks.2.mlp.lin2.bias": "model.safetensors",
350
+ "sam_model.blocks.2.mlp.lin2.weight": "model.safetensors",
351
+ "sam_model.blocks.2.norm1.bias": "model.safetensors",
352
+ "sam_model.blocks.2.norm1.weight": "model.safetensors",
353
+ "sam_model.blocks.2.norm2.bias": "model.safetensors",
354
+ "sam_model.blocks.2.norm2.weight": "model.safetensors",
355
+ "sam_model.blocks.3.attn.proj.bias": "model.safetensors",
356
+ "sam_model.blocks.3.attn.proj.weight": "model.safetensors",
357
+ "sam_model.blocks.3.attn.qkv.bias": "model.safetensors",
358
+ "sam_model.blocks.3.attn.qkv.weight": "model.safetensors",
359
+ "sam_model.blocks.3.attn.rel_pos_h": "model.safetensors",
360
+ "sam_model.blocks.3.attn.rel_pos_w": "model.safetensors",
361
+ "sam_model.blocks.3.mlp.lin1.bias": "model.safetensors",
362
+ "sam_model.blocks.3.mlp.lin1.weight": "model.safetensors",
363
+ "sam_model.blocks.3.mlp.lin2.bias": "model.safetensors",
364
+ "sam_model.blocks.3.mlp.lin2.weight": "model.safetensors",
365
+ "sam_model.blocks.3.norm1.bias": "model.safetensors",
366
+ "sam_model.blocks.3.norm1.weight": "model.safetensors",
367
+ "sam_model.blocks.3.norm2.bias": "model.safetensors",
368
+ "sam_model.blocks.3.norm2.weight": "model.safetensors",
369
+ "sam_model.blocks.4.attn.proj.bias": "model.safetensors",
370
+ "sam_model.blocks.4.attn.proj.weight": "model.safetensors",
371
+ "sam_model.blocks.4.attn.qkv.bias": "model.safetensors",
372
+ "sam_model.blocks.4.attn.qkv.weight": "model.safetensors",
373
+ "sam_model.blocks.4.attn.rel_pos_h": "model.safetensors",
374
+ "sam_model.blocks.4.attn.rel_pos_w": "model.safetensors",
375
+ "sam_model.blocks.4.mlp.lin1.bias": "model.safetensors",
376
+ "sam_model.blocks.4.mlp.lin1.weight": "model.safetensors",
377
+ "sam_model.blocks.4.mlp.lin2.bias": "model.safetensors",
378
+ "sam_model.blocks.4.mlp.lin2.weight": "model.safetensors",
379
+ "sam_model.blocks.4.norm1.bias": "model.safetensors",
380
+ "sam_model.blocks.4.norm1.weight": "model.safetensors",
381
+ "sam_model.blocks.4.norm2.bias": "model.safetensors",
382
+ "sam_model.blocks.4.norm2.weight": "model.safetensors",
383
+ "sam_model.blocks.5.attn.proj.bias": "model.safetensors",
384
+ "sam_model.blocks.5.attn.proj.weight": "model.safetensors",
385
+ "sam_model.blocks.5.attn.qkv.bias": "model.safetensors",
386
+ "sam_model.blocks.5.attn.qkv.weight": "model.safetensors",
387
+ "sam_model.blocks.5.attn.rel_pos_h": "model.safetensors",
388
+ "sam_model.blocks.5.attn.rel_pos_w": "model.safetensors",
389
+ "sam_model.blocks.5.mlp.lin1.bias": "model.safetensors",
390
+ "sam_model.blocks.5.mlp.lin1.weight": "model.safetensors",
391
+ "sam_model.blocks.5.mlp.lin2.bias": "model.safetensors",
392
+ "sam_model.blocks.5.mlp.lin2.weight": "model.safetensors",
393
+ "sam_model.blocks.5.norm1.bias": "model.safetensors",
394
+ "sam_model.blocks.5.norm1.weight": "model.safetensors",
395
+ "sam_model.blocks.5.norm2.bias": "model.safetensors",
396
+ "sam_model.blocks.5.norm2.weight": "model.safetensors",
397
+ "sam_model.blocks.6.attn.proj.bias": "model.safetensors",
398
+ "sam_model.blocks.6.attn.proj.weight": "model.safetensors",
399
+ "sam_model.blocks.6.attn.qkv.bias": "model.safetensors",
400
+ "sam_model.blocks.6.attn.qkv.weight": "model.safetensors",
401
+ "sam_model.blocks.6.attn.rel_pos_h": "model.safetensors",
402
+ "sam_model.blocks.6.attn.rel_pos_w": "model.safetensors",
403
+ "sam_model.blocks.6.mlp.lin1.bias": "model.safetensors",
404
+ "sam_model.blocks.6.mlp.lin1.weight": "model.safetensors",
405
+ "sam_model.blocks.6.mlp.lin2.bias": "model.safetensors",
406
+ "sam_model.blocks.6.mlp.lin2.weight": "model.safetensors",
407
+ "sam_model.blocks.6.norm1.bias": "model.safetensors",
408
+ "sam_model.blocks.6.norm1.weight": "model.safetensors",
409
+ "sam_model.blocks.6.norm2.bias": "model.safetensors",
410
+ "sam_model.blocks.6.norm2.weight": "model.safetensors",
411
+ "sam_model.blocks.7.attn.proj.bias": "model.safetensors",
412
+ "sam_model.blocks.7.attn.proj.weight": "model.safetensors",
413
+ "sam_model.blocks.7.attn.qkv.bias": "model.safetensors",
414
+ "sam_model.blocks.7.attn.qkv.weight": "model.safetensors",
415
+ "sam_model.blocks.7.attn.rel_pos_h": "model.safetensors",
416
+ "sam_model.blocks.7.attn.rel_pos_w": "model.safetensors",
417
+ "sam_model.blocks.7.mlp.lin1.bias": "model.safetensors",
418
+ "sam_model.blocks.7.mlp.lin1.weight": "model.safetensors",
419
+ "sam_model.blocks.7.mlp.lin2.bias": "model.safetensors",
420
+ "sam_model.blocks.7.mlp.lin2.weight": "model.safetensors",
421
+ "sam_model.blocks.7.norm1.bias": "model.safetensors",
422
+ "sam_model.blocks.7.norm1.weight": "model.safetensors",
423
+ "sam_model.blocks.7.norm2.bias": "model.safetensors",
424
+ "sam_model.blocks.7.norm2.weight": "model.safetensors",
425
+ "sam_model.blocks.8.attn.proj.bias": "model.safetensors",
426
+ "sam_model.blocks.8.attn.proj.weight": "model.safetensors",
427
+ "sam_model.blocks.8.attn.qkv.bias": "model.safetensors",
428
+ "sam_model.blocks.8.attn.qkv.weight": "model.safetensors",
429
+ "sam_model.blocks.8.attn.rel_pos_h": "model.safetensors",
430
+ "sam_model.blocks.8.attn.rel_pos_w": "model.safetensors",
431
+ "sam_model.blocks.8.mlp.lin1.bias": "model.safetensors",
432
+ "sam_model.blocks.8.mlp.lin1.weight": "model.safetensors",
433
+ "sam_model.blocks.8.mlp.lin2.bias": "model.safetensors",
434
+ "sam_model.blocks.8.mlp.lin2.weight": "model.safetensors",
435
+ "sam_model.blocks.8.norm1.bias": "model.safetensors",
436
+ "sam_model.blocks.8.norm1.weight": "model.safetensors",
437
+ "sam_model.blocks.8.norm2.bias": "model.safetensors",
438
+ "sam_model.blocks.8.norm2.weight": "model.safetensors",
439
+ "sam_model.blocks.9.attn.proj.bias": "model.safetensors",
440
+ "sam_model.blocks.9.attn.proj.weight": "model.safetensors",
441
+ "sam_model.blocks.9.attn.qkv.bias": "model.safetensors",
442
+ "sam_model.blocks.9.attn.qkv.weight": "model.safetensors",
443
+ "sam_model.blocks.9.attn.rel_pos_h": "model.safetensors",
444
+ "sam_model.blocks.9.attn.rel_pos_w": "model.safetensors",
445
+ "sam_model.blocks.9.mlp.lin1.bias": "model.safetensors",
446
+ "sam_model.blocks.9.mlp.lin1.weight": "model.safetensors",
447
+ "sam_model.blocks.9.mlp.lin2.bias": "model.safetensors",
448
+ "sam_model.blocks.9.mlp.lin2.weight": "model.safetensors",
449
+ "sam_model.blocks.9.norm1.bias": "model.safetensors",
450
+ "sam_model.blocks.9.norm1.weight": "model.safetensors",
451
+ "sam_model.blocks.9.norm2.bias": "model.safetensors",
452
+ "sam_model.blocks.9.norm2.weight": "model.safetensors",
453
+ "sam_model.neck.0.weight": "model.safetensors",
454
+ "sam_model.neck.1.bias": "model.safetensors",
455
+ "sam_model.neck.1.weight": "model.safetensors",
456
+ "sam_model.neck.2.weight": "model.safetensors",
457
+ "sam_model.neck.3.bias": "model.safetensors",
458
+ "sam_model.neck.3.weight": "model.safetensors",
459
+ "sam_model.net_2.weight": "model.safetensors",
460
+ "sam_model.net_3.weight": "model.safetensors",
461
+ "sam_model.patch_embed.proj.bias": "model.safetensors",
462
+ "sam_model.patch_embed.proj.weight": "model.safetensors",
463
+ "sam_model.pos_embed": "model.safetensors",
464
+ "view_separator": "model.safetensors",
465
+ "vision_model.embeddings.class_embedding": "model.safetensors",
466
+ "vision_model.embeddings.patch_embedding.weight": "model.safetensors",
467
+ "vision_model.embeddings.position_embedding.weight": "model.safetensors",
468
+ "vision_model.pre_layrnorm.bias": "model.safetensors",
469
+ "vision_model.pre_layrnorm.weight": "model.safetensors",
470
+ "vision_model.transformer.layers.0.layer_norm1.bias": "model.safetensors",
471
+ "vision_model.transformer.layers.0.layer_norm1.weight": "model.safetensors",
472
+ "vision_model.transformer.layers.0.layer_norm2.bias": "model.safetensors",
473
+ "vision_model.transformer.layers.0.layer_norm2.weight": "model.safetensors",
474
+ "vision_model.transformer.layers.0.mlp.fc1.bias": "model.safetensors",
475
+ "vision_model.transformer.layers.0.mlp.fc1.weight": "model.safetensors",
476
+ "vision_model.transformer.layers.0.mlp.fc2.bias": "model.safetensors",
477
+ "vision_model.transformer.layers.0.mlp.fc2.weight": "model.safetensors",
478
+ "vision_model.transformer.layers.0.self_attn.out_proj.bias": "model.safetensors",
479
+ "vision_model.transformer.layers.0.self_attn.out_proj.weight": "model.safetensors",
480
+ "vision_model.transformer.layers.0.self_attn.qkv_proj.bias": "model.safetensors",
481
+ "vision_model.transformer.layers.0.self_attn.qkv_proj.weight": "model.safetensors",
482
+ "vision_model.transformer.layers.1.layer_norm1.bias": "model.safetensors",
483
+ "vision_model.transformer.layers.1.layer_norm1.weight": "model.safetensors",
484
+ "vision_model.transformer.layers.1.layer_norm2.bias": "model.safetensors",
485
+ "vision_model.transformer.layers.1.layer_norm2.weight": "model.safetensors",
486
+ "vision_model.transformer.layers.1.mlp.fc1.bias": "model.safetensors",
487
+ "vision_model.transformer.layers.1.mlp.fc1.weight": "model.safetensors",
488
+ "vision_model.transformer.layers.1.mlp.fc2.bias": "model.safetensors",
489
+ "vision_model.transformer.layers.1.mlp.fc2.weight": "model.safetensors",
490
+ "vision_model.transformer.layers.1.self_attn.out_proj.bias": "model.safetensors",
491
+ "vision_model.transformer.layers.1.self_attn.out_proj.weight": "model.safetensors",
492
+ "vision_model.transformer.layers.1.self_attn.qkv_proj.bias": "model.safetensors",
493
+ "vision_model.transformer.layers.1.self_attn.qkv_proj.weight": "model.safetensors",
494
+ "vision_model.transformer.layers.10.layer_norm1.bias": "model.safetensors",
495
+ "vision_model.transformer.layers.10.layer_norm1.weight": "model.safetensors",
496
+ "vision_model.transformer.layers.10.layer_norm2.bias": "model.safetensors",
497
+ "vision_model.transformer.layers.10.layer_norm2.weight": "model.safetensors",
498
+ "vision_model.transformer.layers.10.mlp.fc1.bias": "model.safetensors",
499
+ "vision_model.transformer.layers.10.mlp.fc1.weight": "model.safetensors",
500
+ "vision_model.transformer.layers.10.mlp.fc2.bias": "model.safetensors",
501
+ "vision_model.transformer.layers.10.mlp.fc2.weight": "model.safetensors",
502
+ "vision_model.transformer.layers.10.self_attn.out_proj.bias": "model.safetensors",
503
+ "vision_model.transformer.layers.10.self_attn.out_proj.weight": "model.safetensors",
504
+ "vision_model.transformer.layers.10.self_attn.qkv_proj.bias": "model.safetensors",
505
+ "vision_model.transformer.layers.10.self_attn.qkv_proj.weight": "model.safetensors",
506
+ "vision_model.transformer.layers.11.layer_norm1.bias": "model.safetensors",
507
+ "vision_model.transformer.layers.11.layer_norm1.weight": "model.safetensors",
508
+ "vision_model.transformer.layers.11.layer_norm2.bias": "model.safetensors",
509
+ "vision_model.transformer.layers.11.layer_norm2.weight": "model.safetensors",
510
+ "vision_model.transformer.layers.11.mlp.fc1.bias": "model.safetensors",
511
+ "vision_model.transformer.layers.11.mlp.fc1.weight": "model.safetensors",
512
+ "vision_model.transformer.layers.11.mlp.fc2.bias": "model.safetensors",
513
+ "vision_model.transformer.layers.11.mlp.fc2.weight": "model.safetensors",
514
+ "vision_model.transformer.layers.11.self_attn.out_proj.bias": "model.safetensors",
515
+ "vision_model.transformer.layers.11.self_attn.out_proj.weight": "model.safetensors",
516
+ "vision_model.transformer.layers.11.self_attn.qkv_proj.bias": "model.safetensors",
517
+ "vision_model.transformer.layers.11.self_attn.qkv_proj.weight": "model.safetensors",
518
+ "vision_model.transformer.layers.12.layer_norm1.bias": "model.safetensors",
519
+ "vision_model.transformer.layers.12.layer_norm1.weight": "model.safetensors",
520
+ "vision_model.transformer.layers.12.layer_norm2.bias": "model.safetensors",
521
+ "vision_model.transformer.layers.12.layer_norm2.weight": "model.safetensors",
522
+ "vision_model.transformer.layers.12.mlp.fc1.bias": "model.safetensors",
523
+ "vision_model.transformer.layers.12.mlp.fc1.weight": "model.safetensors",
524
+ "vision_model.transformer.layers.12.mlp.fc2.bias": "model.safetensors",
525
+ "vision_model.transformer.layers.12.mlp.fc2.weight": "model.safetensors",
526
+ "vision_model.transformer.layers.12.self_attn.out_proj.bias": "model.safetensors",
527
+ "vision_model.transformer.layers.12.self_attn.out_proj.weight": "model.safetensors",
528
+ "vision_model.transformer.layers.12.self_attn.qkv_proj.bias": "model.safetensors",
529
+ "vision_model.transformer.layers.12.self_attn.qkv_proj.weight": "model.safetensors",
530
+ "vision_model.transformer.layers.13.layer_norm1.bias": "model.safetensors",
531
+ "vision_model.transformer.layers.13.layer_norm1.weight": "model.safetensors",
532
+ "vision_model.transformer.layers.13.layer_norm2.bias": "model.safetensors",
533
+ "vision_model.transformer.layers.13.layer_norm2.weight": "model.safetensors",
534
+ "vision_model.transformer.layers.13.mlp.fc1.bias": "model.safetensors",
535
+ "vision_model.transformer.layers.13.mlp.fc1.weight": "model.safetensors",
536
+ "vision_model.transformer.layers.13.mlp.fc2.bias": "model.safetensors",
537
+ "vision_model.transformer.layers.13.mlp.fc2.weight": "model.safetensors",
538
+ "vision_model.transformer.layers.13.self_attn.out_proj.bias": "model.safetensors",
539
+ "vision_model.transformer.layers.13.self_attn.out_proj.weight": "model.safetensors",
540
+ "vision_model.transformer.layers.13.self_attn.qkv_proj.bias": "model.safetensors",
541
+ "vision_model.transformer.layers.13.self_attn.qkv_proj.weight": "model.safetensors",
542
+ "vision_model.transformer.layers.14.layer_norm1.bias": "model.safetensors",
543
+ "vision_model.transformer.layers.14.layer_norm1.weight": "model.safetensors",
544
+ "vision_model.transformer.layers.14.layer_norm2.bias": "model.safetensors",
545
+ "vision_model.transformer.layers.14.layer_norm2.weight": "model.safetensors",
546
+ "vision_model.transformer.layers.14.mlp.fc1.bias": "model.safetensors",
547
+ "vision_model.transformer.layers.14.mlp.fc1.weight": "model.safetensors",
548
+ "vision_model.transformer.layers.14.mlp.fc2.bias": "model.safetensors",
549
+ "vision_model.transformer.layers.14.mlp.fc2.weight": "model.safetensors",
550
+ "vision_model.transformer.layers.14.self_attn.out_proj.bias": "model.safetensors",
551
+ "vision_model.transformer.layers.14.self_attn.out_proj.weight": "model.safetensors",
552
+ "vision_model.transformer.layers.14.self_attn.qkv_proj.bias": "model.safetensors",
553
+ "vision_model.transformer.layers.14.self_attn.qkv_proj.weight": "model.safetensors",
554
+ "vision_model.transformer.layers.15.layer_norm1.bias": "model.safetensors",
555
+ "vision_model.transformer.layers.15.layer_norm1.weight": "model.safetensors",
556
+ "vision_model.transformer.layers.15.layer_norm2.bias": "model.safetensors",
557
+ "vision_model.transformer.layers.15.layer_norm2.weight": "model.safetensors",
558
+ "vision_model.transformer.layers.15.mlp.fc1.bias": "model.safetensors",
559
+ "vision_model.transformer.layers.15.mlp.fc1.weight": "model.safetensors",
560
+ "vision_model.transformer.layers.15.mlp.fc2.bias": "model.safetensors",
561
+ "vision_model.transformer.layers.15.mlp.fc2.weight": "model.safetensors",
562
+ "vision_model.transformer.layers.15.self_attn.out_proj.bias": "model.safetensors",
563
+ "vision_model.transformer.layers.15.self_attn.out_proj.weight": "model.safetensors",
564
+ "vision_model.transformer.layers.15.self_attn.qkv_proj.bias": "model.safetensors",
565
+ "vision_model.transformer.layers.15.self_attn.qkv_proj.weight": "model.safetensors",
566
+ "vision_model.transformer.layers.16.layer_norm1.bias": "model.safetensors",
567
+ "vision_model.transformer.layers.16.layer_norm1.weight": "model.safetensors",
568
+ "vision_model.transformer.layers.16.layer_norm2.bias": "model.safetensors",
569
+ "vision_model.transformer.layers.16.layer_norm2.weight": "model.safetensors",
570
+ "vision_model.transformer.layers.16.mlp.fc1.bias": "model.safetensors",
571
+ "vision_model.transformer.layers.16.mlp.fc1.weight": "model.safetensors",
572
+ "vision_model.transformer.layers.16.mlp.fc2.bias": "model.safetensors",
573
+ "vision_model.transformer.layers.16.mlp.fc2.weight": "model.safetensors",
574
+ "vision_model.transformer.layers.16.self_attn.out_proj.bias": "model.safetensors",
575
+ "vision_model.transformer.layers.16.self_attn.out_proj.weight": "model.safetensors",
576
+ "vision_model.transformer.layers.16.self_attn.qkv_proj.bias": "model.safetensors",
577
+ "vision_model.transformer.layers.16.self_attn.qkv_proj.weight": "model.safetensors",
578
+ "vision_model.transformer.layers.17.layer_norm1.bias": "model.safetensors",
579
+ "vision_model.transformer.layers.17.layer_norm1.weight": "model.safetensors",
580
+ "vision_model.transformer.layers.17.layer_norm2.bias": "model.safetensors",
581
+ "vision_model.transformer.layers.17.layer_norm2.weight": "model.safetensors",
582
+ "vision_model.transformer.layers.17.mlp.fc1.bias": "model.safetensors",
583
+ "vision_model.transformer.layers.17.mlp.fc1.weight": "model.safetensors",
584
+ "vision_model.transformer.layers.17.mlp.fc2.bias": "model.safetensors",
585
+ "vision_model.transformer.layers.17.mlp.fc2.weight": "model.safetensors",
586
+ "vision_model.transformer.layers.17.self_attn.out_proj.bias": "model.safetensors",
587
+ "vision_model.transformer.layers.17.self_attn.out_proj.weight": "model.safetensors",
588
+ "vision_model.transformer.layers.17.self_attn.qkv_proj.bias": "model.safetensors",
589
+ "vision_model.transformer.layers.17.self_attn.qkv_proj.weight": "model.safetensors",
590
+ "vision_model.transformer.layers.18.layer_norm1.bias": "model.safetensors",
591
+ "vision_model.transformer.layers.18.layer_norm1.weight": "model.safetensors",
592
+ "vision_model.transformer.layers.18.layer_norm2.bias": "model.safetensors",
593
+ "vision_model.transformer.layers.18.layer_norm2.weight": "model.safetensors",
594
+ "vision_model.transformer.layers.18.mlp.fc1.bias": "model.safetensors",
595
+ "vision_model.transformer.layers.18.mlp.fc1.weight": "model.safetensors",
596
+ "vision_model.transformer.layers.18.mlp.fc2.bias": "model.safetensors",
597
+ "vision_model.transformer.layers.18.mlp.fc2.weight": "model.safetensors",
598
+ "vision_model.transformer.layers.18.self_attn.out_proj.bias": "model.safetensors",
599
+ "vision_model.transformer.layers.18.self_attn.out_proj.weight": "model.safetensors",
600
+ "vision_model.transformer.layers.18.self_attn.qkv_proj.bias": "model.safetensors",
601
+ "vision_model.transformer.layers.18.self_attn.qkv_proj.weight": "model.safetensors",
602
+ "vision_model.transformer.layers.19.layer_norm1.bias": "model.safetensors",
603
+ "vision_model.transformer.layers.19.layer_norm1.weight": "model.safetensors",
604
+ "vision_model.transformer.layers.19.layer_norm2.bias": "model.safetensors",
605
+ "vision_model.transformer.layers.19.layer_norm2.weight": "model.safetensors",
606
+ "vision_model.transformer.layers.19.mlp.fc1.bias": "model.safetensors",
607
+ "vision_model.transformer.layers.19.mlp.fc1.weight": "model.safetensors",
608
+ "vision_model.transformer.layers.19.mlp.fc2.bias": "model.safetensors",
609
+ "vision_model.transformer.layers.19.mlp.fc2.weight": "model.safetensors",
610
+ "vision_model.transformer.layers.19.self_attn.out_proj.bias": "model.safetensors",
611
+ "vision_model.transformer.layers.19.self_attn.out_proj.weight": "model.safetensors",
612
+ "vision_model.transformer.layers.19.self_attn.qkv_proj.bias": "model.safetensors",
613
+ "vision_model.transformer.layers.19.self_attn.qkv_proj.weight": "model.safetensors",
614
+ "vision_model.transformer.layers.2.layer_norm1.bias": "model.safetensors",
615
+ "vision_model.transformer.layers.2.layer_norm1.weight": "model.safetensors",
616
+ "vision_model.transformer.layers.2.layer_norm2.bias": "model.safetensors",
617
+ "vision_model.transformer.layers.2.layer_norm2.weight": "model.safetensors",
618
+ "vision_model.transformer.layers.2.mlp.fc1.bias": "model.safetensors",
619
+ "vision_model.transformer.layers.2.mlp.fc1.weight": "model.safetensors",
620
+ "vision_model.transformer.layers.2.mlp.fc2.bias": "model.safetensors",
621
+ "vision_model.transformer.layers.2.mlp.fc2.weight": "model.safetensors",
622
+ "vision_model.transformer.layers.2.self_attn.out_proj.bias": "model.safetensors",
623
+ "vision_model.transformer.layers.2.self_attn.out_proj.weight": "model.safetensors",
624
+ "vision_model.transformer.layers.2.self_attn.qkv_proj.bias": "model.safetensors",
625
+ "vision_model.transformer.layers.2.self_attn.qkv_proj.weight": "model.safetensors",
626
+ "vision_model.transformer.layers.20.layer_norm1.bias": "model.safetensors",
627
+ "vision_model.transformer.layers.20.layer_norm1.weight": "model.safetensors",
628
+ "vision_model.transformer.layers.20.layer_norm2.bias": "model.safetensors",
629
+ "vision_model.transformer.layers.20.layer_norm2.weight": "model.safetensors",
630
+ "vision_model.transformer.layers.20.mlp.fc1.bias": "model.safetensors",
631
+ "vision_model.transformer.layers.20.mlp.fc1.weight": "model.safetensors",
632
+ "vision_model.transformer.layers.20.mlp.fc2.bias": "model.safetensors",
633
+ "vision_model.transformer.layers.20.mlp.fc2.weight": "model.safetensors",
634
+ "vision_model.transformer.layers.20.self_attn.out_proj.bias": "model.safetensors",
635
+ "vision_model.transformer.layers.20.self_attn.out_proj.weight": "model.safetensors",
636
+ "vision_model.transformer.layers.20.self_attn.qkv_proj.bias": "model.safetensors",
637
+ "vision_model.transformer.layers.20.self_attn.qkv_proj.weight": "model.safetensors",
638
+ "vision_model.transformer.layers.21.layer_norm1.bias": "model.safetensors",
639
+ "vision_model.transformer.layers.21.layer_norm1.weight": "model.safetensors",
640
+ "vision_model.transformer.layers.21.layer_norm2.bias": "model.safetensors",
641
+ "vision_model.transformer.layers.21.layer_norm2.weight": "model.safetensors",
642
+ "vision_model.transformer.layers.21.mlp.fc1.bias": "model.safetensors",
643
+ "vision_model.transformer.layers.21.mlp.fc1.weight": "model.safetensors",
644
+ "vision_model.transformer.layers.21.mlp.fc2.bias": "model.safetensors",
645
+ "vision_model.transformer.layers.21.mlp.fc2.weight": "model.safetensors",
646
+ "vision_model.transformer.layers.21.self_attn.out_proj.bias": "model.safetensors",
647
+ "vision_model.transformer.layers.21.self_attn.out_proj.weight": "model.safetensors",
648
+ "vision_model.transformer.layers.21.self_attn.qkv_proj.bias": "model.safetensors",
649
+ "vision_model.transformer.layers.21.self_attn.qkv_proj.weight": "model.safetensors",
650
+ "vision_model.transformer.layers.22.layer_norm1.bias": "model.safetensors",
651
+ "vision_model.transformer.layers.22.layer_norm1.weight": "model.safetensors",
652
+ "vision_model.transformer.layers.22.layer_norm2.bias": "model.safetensors",
653
+ "vision_model.transformer.layers.22.layer_norm2.weight": "model.safetensors",
654
+ "vision_model.transformer.layers.22.mlp.fc1.bias": "model.safetensors",
655
+ "vision_model.transformer.layers.22.mlp.fc1.weight": "model.safetensors",
656
+ "vision_model.transformer.layers.22.mlp.fc2.bias": "model.safetensors",
657
+ "vision_model.transformer.layers.22.mlp.fc2.weight": "model.safetensors",
658
+ "vision_model.transformer.layers.22.self_attn.out_proj.bias": "model.safetensors",
659
+ "vision_model.transformer.layers.22.self_attn.out_proj.weight": "model.safetensors",
660
+ "vision_model.transformer.layers.22.self_attn.qkv_proj.bias": "model.safetensors",
661
+ "vision_model.transformer.layers.22.self_attn.qkv_proj.weight": "model.safetensors",
662
+ "vision_model.transformer.layers.23.layer_norm1.bias": "model.safetensors",
663
+ "vision_model.transformer.layers.23.layer_norm1.weight": "model.safetensors",
664
+ "vision_model.transformer.layers.23.layer_norm2.bias": "model.safetensors",
665
+ "vision_model.transformer.layers.23.layer_norm2.weight": "model.safetensors",
666
+ "vision_model.transformer.layers.23.mlp.fc1.bias": "model.safetensors",
667
+ "vision_model.transformer.layers.23.mlp.fc1.weight": "model.safetensors",
668
+ "vision_model.transformer.layers.23.mlp.fc2.bias": "model.safetensors",
669
+ "vision_model.transformer.layers.23.mlp.fc2.weight": "model.safetensors",
670
+ "vision_model.transformer.layers.23.self_attn.out_proj.bias": "model.safetensors",
671
+ "vision_model.transformer.layers.23.self_attn.out_proj.weight": "model.safetensors",
672
+ "vision_model.transformer.layers.23.self_attn.qkv_proj.bias": "model.safetensors",
673
+ "vision_model.transformer.layers.23.self_attn.qkv_proj.weight": "model.safetensors",
674
+ "vision_model.transformer.layers.3.layer_norm1.bias": "model.safetensors",
675
+ "vision_model.transformer.layers.3.layer_norm1.weight": "model.safetensors",
676
+ "vision_model.transformer.layers.3.layer_norm2.bias": "model.safetensors",
677
+ "vision_model.transformer.layers.3.layer_norm2.weight": "model.safetensors",
678
+ "vision_model.transformer.layers.3.mlp.fc1.bias": "model.safetensors",
679
+ "vision_model.transformer.layers.3.mlp.fc1.weight": "model.safetensors",
680
+ "vision_model.transformer.layers.3.mlp.fc2.bias": "model.safetensors",
681
+ "vision_model.transformer.layers.3.mlp.fc2.weight": "model.safetensors",
682
+ "vision_model.transformer.layers.3.self_attn.out_proj.bias": "model.safetensors",
683
+ "vision_model.transformer.layers.3.self_attn.out_proj.weight": "model.safetensors",
684
+ "vision_model.transformer.layers.3.self_attn.qkv_proj.bias": "model.safetensors",
685
+ "vision_model.transformer.layers.3.self_attn.qkv_proj.weight": "model.safetensors",
686
+ "vision_model.transformer.layers.4.layer_norm1.bias": "model.safetensors",
687
+ "vision_model.transformer.layers.4.layer_norm1.weight": "model.safetensors",
688
+ "vision_model.transformer.layers.4.layer_norm2.bias": "model.safetensors",
689
+ "vision_model.transformer.layers.4.layer_norm2.weight": "model.safetensors",
690
+ "vision_model.transformer.layers.4.mlp.fc1.bias": "model.safetensors",
691
+ "vision_model.transformer.layers.4.mlp.fc1.weight": "model.safetensors",
692
+ "vision_model.transformer.layers.4.mlp.fc2.bias": "model.safetensors",
693
+ "vision_model.transformer.layers.4.mlp.fc2.weight": "model.safetensors",
694
+ "vision_model.transformer.layers.4.self_attn.out_proj.bias": "model.safetensors",
695
+ "vision_model.transformer.layers.4.self_attn.out_proj.weight": "model.safetensors",
696
+ "vision_model.transformer.layers.4.self_attn.qkv_proj.bias": "model.safetensors",
697
+ "vision_model.transformer.layers.4.self_attn.qkv_proj.weight": "model.safetensors",
698
+ "vision_model.transformer.layers.5.layer_norm1.bias": "model.safetensors",
699
+ "vision_model.transformer.layers.5.layer_norm1.weight": "model.safetensors",
700
+ "vision_model.transformer.layers.5.layer_norm2.bias": "model.safetensors",
701
+ "vision_model.transformer.layers.5.layer_norm2.weight": "model.safetensors",
702
+ "vision_model.transformer.layers.5.mlp.fc1.bias": "model.safetensors",
703
+ "vision_model.transformer.layers.5.mlp.fc1.weight": "model.safetensors",
704
+ "vision_model.transformer.layers.5.mlp.fc2.bias": "model.safetensors",
705
+ "vision_model.transformer.layers.5.mlp.fc2.weight": "model.safetensors",
706
+ "vision_model.transformer.layers.5.self_attn.out_proj.bias": "model.safetensors",
707
+ "vision_model.transformer.layers.5.self_attn.out_proj.weight": "model.safetensors",
708
+ "vision_model.transformer.layers.5.self_attn.qkv_proj.bias": "model.safetensors",
709
+ "vision_model.transformer.layers.5.self_attn.qkv_proj.weight": "model.safetensors",
710
+ "vision_model.transformer.layers.6.layer_norm1.bias": "model.safetensors",
711
+ "vision_model.transformer.layers.6.layer_norm1.weight": "model.safetensors",
712
+ "vision_model.transformer.layers.6.layer_norm2.bias": "model.safetensors",
713
+ "vision_model.transformer.layers.6.layer_norm2.weight": "model.safetensors",
714
+ "vision_model.transformer.layers.6.mlp.fc1.bias": "model.safetensors",
715
+ "vision_model.transformer.layers.6.mlp.fc1.weight": "model.safetensors",
716
+ "vision_model.transformer.layers.6.mlp.fc2.bias": "model.safetensors",
717
+ "vision_model.transformer.layers.6.mlp.fc2.weight": "model.safetensors",
718
+ "vision_model.transformer.layers.6.self_attn.out_proj.bias": "model.safetensors",
719
+ "vision_model.transformer.layers.6.self_attn.out_proj.weight": "model.safetensors",
720
+ "vision_model.transformer.layers.6.self_attn.qkv_proj.bias": "model.safetensors",
721
+ "vision_model.transformer.layers.6.self_attn.qkv_proj.weight": "model.safetensors",
722
+ "vision_model.transformer.layers.7.layer_norm1.bias": "model.safetensors",
723
+ "vision_model.transformer.layers.7.layer_norm1.weight": "model.safetensors",
724
+ "vision_model.transformer.layers.7.layer_norm2.bias": "model.safetensors",
725
+ "vision_model.transformer.layers.7.layer_norm2.weight": "model.safetensors",
726
+ "vision_model.transformer.layers.7.mlp.fc1.bias": "model.safetensors",
727
+ "vision_model.transformer.layers.7.mlp.fc1.weight": "model.safetensors",
728
+ "vision_model.transformer.layers.7.mlp.fc2.bias": "model.safetensors",
729
+ "vision_model.transformer.layers.7.mlp.fc2.weight": "model.safetensors",
730
+ "vision_model.transformer.layers.7.self_attn.out_proj.bias": "model.safetensors",
731
+ "vision_model.transformer.layers.7.self_attn.out_proj.weight": "model.safetensors",
732
+ "vision_model.transformer.layers.7.self_attn.qkv_proj.bias": "model.safetensors",
733
+ "vision_model.transformer.layers.7.self_attn.qkv_proj.weight": "model.safetensors",
734
+ "vision_model.transformer.layers.8.layer_norm1.bias": "model.safetensors",
735
+ "vision_model.transformer.layers.8.layer_norm1.weight": "model.safetensors",
736
+ "vision_model.transformer.layers.8.layer_norm2.bias": "model.safetensors",
737
+ "vision_model.transformer.layers.8.layer_norm2.weight": "model.safetensors",
738
+ "vision_model.transformer.layers.8.mlp.fc1.bias": "model.safetensors",
739
+ "vision_model.transformer.layers.8.mlp.fc1.weight": "model.safetensors",
740
+ "vision_model.transformer.layers.8.mlp.fc2.bias": "model.safetensors",
741
+ "vision_model.transformer.layers.8.mlp.fc2.weight": "model.safetensors",
742
+ "vision_model.transformer.layers.8.self_attn.out_proj.bias": "model.safetensors",
743
+ "vision_model.transformer.layers.8.self_attn.out_proj.weight": "model.safetensors",
744
+ "vision_model.transformer.layers.8.self_attn.qkv_proj.bias": "model.safetensors",
745
+ "vision_model.transformer.layers.8.self_attn.qkv_proj.weight": "model.safetensors",
746
+ "vision_model.transformer.layers.9.layer_norm1.bias": "model.safetensors",
747
+ "vision_model.transformer.layers.9.layer_norm1.weight": "model.safetensors",
748
+ "vision_model.transformer.layers.9.layer_norm2.bias": "model.safetensors",
749
+ "vision_model.transformer.layers.9.layer_norm2.weight": "model.safetensors",
750
+ "vision_model.transformer.layers.9.mlp.fc1.bias": "model.safetensors",
751
+ "vision_model.transformer.layers.9.mlp.fc1.weight": "model.safetensors",
752
+ "vision_model.transformer.layers.9.mlp.fc2.bias": "model.safetensors",
753
+ "vision_model.transformer.layers.9.mlp.fc2.weight": "model.safetensors",
754
+ "vision_model.transformer.layers.9.self_attn.out_proj.bias": "model.safetensors",
755
+ "vision_model.transformer.layers.9.self_attn.out_proj.weight": "model.safetensors",
756
+ "vision_model.transformer.layers.9.self_attn.qkv_proj.bias": "model.safetensors",
757
+ "vision_model.transformer.layers.9.self_attn.qkv_proj.weight": "model.safetensors"
758
+ }
759
+ }
precision_map.json ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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",
7
+ "language_model.model.layers.*.input_layernorm": "bfloat16",
8
+ "language_model.model.layers.*.post_attention_layernorm": "bfloat16",
9
+ "language_model.model.layers.*.self_attn.q_proj": "mxfp8",
10
+ "language_model.model.layers.*.self_attn.k_proj": "mxfp8",
11
+ "language_model.model.layers.*.self_attn.v_proj": "mxfp8",
12
+ "language_model.model.layers.*.self_attn.o_proj": "mxfp8",
13
+ "language_model.model.layers.*.mlp.gate_proj": "mxfp8",
14
+ "language_model.model.layers.*.mlp.up_proj": "mxfp8",
15
+ "language_model.model.layers.*.mlp.down_proj": "mxfp8",
16
+ "language_model.model.layers.*.mlp.shared_experts.gate_proj": "mxfp8",
17
+ "language_model.model.layers.*.mlp.shared_experts.up_proj": "mxfp8",
18
+ "language_model.model.layers.*.mlp.shared_experts.down_proj": "mxfp8",
19
+ "language_model.model.layers.*.mlp.switch_mlp.gate_proj": "mxfp8",
20
+ "language_model.model.layers.*.mlp.switch_mlp.up_proj": "mxfp8",
21
+ "language_model.model.layers.*.mlp.switch_mlp.down_proj": "mxfp8",
22
+ "language_model.model.layers.*.mlp.gate": "bfloat16",
23
+ "sam_model": "bfloat16",
24
+ "_generated_from": {
25
+ "source_model": "baidu/Unlimited-OCR",
26
+ "thresholds": {
27
+ "cer_delta": 0.02,
28
+ "digit_cer_delta": 0.02,
29
+ "table_score_degradation": 0.01
30
+ },
31
+ "decisions": {
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": {
48
+ "precision": "mxfp8",
49
+ "reasons": [
50
+ "all measured quality deltas were non-degrading"
51
+ ]
52
+ },
53
+ "attention_q_proj": {
54
+ "precision": "base-map",
55
+ "reasons": [
56
+ "within thresholds"
57
+ ]
58
+ },
59
+ "attention_k_proj": {
60
+ "precision": "base-map",
61
+ "reasons": [
62
+ "within thresholds"
63
+ ]
64
+ },
65
+ "attention_v_proj": {
66
+ "precision": "base-map",
67
+ "reasons": [
68
+ "within thresholds"
69
+ ]
70
+ },
71
+ "attention_o_proj": {
72
+ "precision": "base-map",
73
+ "reasons": [
74
+ "within thresholds"
75
+ ]
76
+ },
77
+ "dense_mlp": {
78
+ "precision": "base-map",
79
+ "reasons": [
80
+ "within thresholds"
81
+ ]
82
+ },
83
+ "shared_experts": {
84
+ "precision": "base-map",
85
+ "reasons": [
86
+ "within thresholds"
87
+ ]
88
+ },
89
+ "routed_experts": {
90
+ "precision": "base-map",
91
+ "reasons": [
92
+ "within thresholds"
93
+ ]
94
+ },
95
+ "lm_head": {
96
+ "precision": "affine8",
97
+ "reasons": [
98
+ "selected by joint quality/throughput calibration: affine8-head"
99
+ ]
100
+ }
101
+ },
102
+ "calibration": {
103
+ "selected": {
104
+ "label": "affine8-head",
105
+ "precision": "affine8"
106
+ },
107
+ "selection_policy": "fastest candidate passing existing quality and throughput limits"
108
+ }
109
+ }
110
+ }
preprocessing_config.json ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "default_mode": "document",
3
+ "modes": {
4
+ "none": {
5
+ "description": "No preprocessing (passthrough)",
6
+ "deskew": false,
7
+ "clahe": false,
8
+ "binarize": false,
9
+ "gamma_correction": false,
10
+ "sharpen": false,
11
+ "grayscale": false,
12
+ "target_size": null
13
+ },
14
+ "auto": {
15
+ "description": "Inspect image and apply needed fixes automatically",
16
+ "deskew": true,
17
+ "clahe": "auto",
18
+ "binarize": false,
19
+ "gamma_correction": "auto",
20
+ "sharpen": "auto",
21
+ "grayscale": false,
22
+ "target_size": 1024
23
+ },
24
+ "document": {
25
+ "description": "Deskew + CLAHE contrast enhancement (default for general documents)",
26
+ "deskew": true,
27
+ "clahe": true,
28
+ "binarize": false,
29
+ "gamma_correction": false,
30
+ "sharpen": false,
31
+ "grayscale": false,
32
+ "target_size": 1024,
33
+ "clahe_clip_limit": 2.0,
34
+ "clahe_grid_size": 8
35
+ },
36
+ "low-contrast": {
37
+ "description": "Aggressive CLAHE + gamma correction for faded scans",
38
+ "deskew": true,
39
+ "clahe": true,
40
+ "binarize": false,
41
+ "gamma_correction": true,
42
+ "sharpen": false,
43
+ "grayscale": false,
44
+ "target_size": 1024,
45
+ "clahe_clip_limit": 4.0,
46
+ "gamma_value": 1.5
47
+ },
48
+ "grayscale": {
49
+ "description": "Convert to grayscale + Otsu binarization",
50
+ "deskew": true,
51
+ "clahe": false,
52
+ "binarize": true,
53
+ "gamma_correction": false,
54
+ "sharpen": false,
55
+ "grayscale": true,
56
+ "target_size": 1024
57
+ },
58
+ "high-resolution": {
59
+ "description": "Resize longest side to 1024 with Lanczos + sharpen",
60
+ "deskew": false,
61
+ "clahe": true,
62
+ "binarize": false,
63
+ "gamma_correction": false,
64
+ "sharpen": true,
65
+ "grayscale": false,
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,
73
+ "gamma_correction": false,
74
+ "sharpen": true,
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."
81
+ }
processor_config.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_special_token": false,
3
+ "candidate_resolutions": [
4
+ [
5
+ 1024,
6
+ 1024
7
+ ]
8
+ ],
9
+ "downsample_ratio": 4,
10
+ "ignore_id": -100,
11
+ "image_mean": [
12
+ 0.5,
13
+ 0.5,
14
+ 0.5
15
+ ],
16
+ "image_std": [
17
+ 0.5,
18
+ 0.5,
19
+ 0.5
20
+ ],
21
+ "image_token": "<image>",
22
+ "mask_prompt": false,
23
+ "normalize": true,
24
+ "pad_token": "<|▁pad▁|>",
25
+ "patch_size": 16,
26
+ "processor_class": "UnlimitedOCRHFProcessor",
27
+ "sft_format": "unlimitedocr"
28
+ }
pyproject.toml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ 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",
19
+ "License :: OSI Approved :: MIT License",
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",
33
+ # mlx-vlm already requires the non-headless OpenCV distribution. Requiring
34
+ # the headless wheel as well installs two distributions that own `cv2`.
35
+ "opencv-python>=4.12",
36
+ "click>=8.0",
37
+ "rich>=13.0",
38
+ "huggingface-hub>=0.20",
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
+
45
+ [project.scripts]
46
+ unlimited-ocr = "unlimited_ocr.cli:main"
47
+
48
+ [project.urls]
49
+ Homepage = "https://huggingface.co/AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
50
+ Repository = "https://huggingface.co/AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/unlimited_ocr"]
quantization/calibrate_precision.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Select a mixed-precision override from measured candidate experiments."""
3
+
4
+ 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"}
19
+
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(
27
+ bf16_accuracy: dict,
28
+ reference_performance: dict,
29
+ experiments: list[dict],
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:
45
+ label = experiment.get("label")
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):
54
+ raise ValueError(f"Calibration metrics are missing for {label}")
55
+ candidate_cer = _number(accuracy, "mean_cer")
56
+ candidate_digit = _number(accuracy, "mean_digit_cer")
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",
97
+ "thresholds": limits,
98
+ "experiments": evaluated,
99
+ "selected": {
100
+ "label": selected["label"],
101
+ "precision": selected["precision"],
102
+ },
103
+ "precision_overrides": {
104
+ "language_model.lm_head": selected["precision"],
105
+ },
106
+ }
107
+
108
+
109
+ 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",
116
+ nargs=4,
117
+ metavar=("LABEL", "PRECISION", "ACCURACY_JSON", "PERFORMANCE_JSON"),
118
+ required=True,
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
+
141
+ if __name__ == "__main__":
142
+ main()
quantization/layer_sensitivity.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Layer sensitivity analysis for OCR-aware mixed-precision quantization.
2
+
3
+ This script quantizes one layer group at a time, runs inference on a small
4
+ evaluation set, and measures the quality delta (CER, digit error, table structure)
5
+ relative to the BF16 baseline.
6
+
7
+ Usage:
8
+ python quantization/layer_sensitivity.py \
9
+ --model-path baidu/Unlimited-OCR \
10
+ --eval-dir ./eval_images/ \
11
+ --output sensitivity_results.json
12
+
13
+ Requires: mlx-vlm, mlx, Pillow, numpy
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ from collections import Counter
20
+ import gc
21
+ import json
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+
28
+ 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
41
+ # MoE router are raw arrays rather than quantizable MLX layers, so they remain
42
+ # BF16 by construction and are not presented as runnable sensitivity groups.
43
+ LAYER_GROUPS: dict[str, tuple[str, ...]] = {
44
+ "vision_encoder": ("vision_model", "sam_model"),
45
+ "vision_projector": ("projector",),
46
+ "token_embeddings": ("language_model.model.embed_tokens",),
47
+ "attention_q_proj": ("language_model.model.layers.*.self_attn.q_proj",),
48
+ "attention_k_proj": ("language_model.model.layers.*.self_attn.k_proj",),
49
+ "attention_v_proj": ("language_model.model.layers.*.self_attn.v_proj",),
50
+ "attention_o_proj": ("language_model.model.layers.*.self_attn.o_proj",),
51
+ "dense_mlp": (
52
+ "language_model.model.layers.*.mlp.gate_proj",
53
+ "language_model.model.layers.*.mlp.up_proj",
54
+ "language_model.model.layers.*.mlp.down_proj",
55
+ ),
56
+ "shared_experts": ("language_model.model.layers.*.mlp.shared_experts",),
57
+ "routed_experts": ("language_model.model.layers.*.mlp.switch_mlp",),
58
+ "lm_head": ("language_model.lm_head",),
59
+ }
60
+
61
+ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".bmp"}
62
+
63
+
64
+ def collect_evaluation_files(
65
+ eval_dir: Path,
66
+ prompt: str = "<image>document parsing.",
67
+ max_tokens: int = 4096,
68
+ ) -> list[tuple[Path, Path]]:
69
+ """Validate an evaluation dataset and return complete image/text pairs."""
70
+ images_dir = eval_dir / "images"
71
+ gt_dir = eval_dir / "ground_truth"
72
+ if not images_dir.is_dir():
73
+ raise FileNotFoundError(f"Images directory not found: {images_dir}")
74
+ if not gt_dir.is_dir():
75
+ raise FileNotFoundError(f"Ground-truth directory not found: {gt_dir}")
76
+ if not isinstance(prompt, str) or prompt.count("<image>") != 1:
77
+ raise ValueError("prompt must contain exactly one literal '<image>' token")
78
+ if not isinstance(max_tokens, int) or isinstance(max_tokens, bool) or max_tokens < 1:
79
+ raise ValueError("max_tokens must be a positive integer")
80
+
81
+ image_files = sorted(
82
+ path for path in images_dir.iterdir()
83
+ if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS
84
+ )
85
+ if not image_files:
86
+ raise ValueError(f"No supported images found in {images_dir}")
87
+
88
+ duplicate_stems = sorted(
89
+ stem for stem, count in Counter(path.stem for path in image_files).items()
90
+ if count > 1
91
+ )
92
+ if duplicate_stems:
93
+ raise ValueError(
94
+ "Multiple input images share the same stem: "
95
+ + ", ".join(duplicate_stems)
96
+ )
97
+
98
+ missing = [
99
+ path.name for path in image_files
100
+ if not (gt_dir / f"{path.stem}.txt").is_file()
101
+ ]
102
+ if missing:
103
+ raise FileNotFoundError(
104
+ "Missing ground-truth text for: " + ", ".join(missing)
105
+ )
106
+ return [(path, gt_dir / f"{path.stem}.txt") for path in image_files]
107
+
108
+
109
+ def compute_cer(reference: str, hypothesis: str) -> float:
110
+ """Compute Character Error Rate with the shared memory-efficient metric."""
111
+ return _compute_cer(reference, hypothesis)
112
+
113
+
114
+ def compute_digit_cer(reference: str, hypothesis: str) -> float:
115
+ """Compute CER only on digit characters."""
116
+ return _compute_digit_cer(reference, hypothesis)
117
+
118
+
119
+ def run_inference(model, processor, image_path: str, prompt: str, max_tokens: int = 4096) -> str:
120
+ """Run a single inference and return the text output."""
121
+ from mlx_vlm import generate
122
+
123
+ response = generate(
124
+ model, processor,
125
+ prompt=prompt,
126
+ image=[image_path],
127
+ max_tokens=max_tokens,
128
+ temperature=0.0,
129
+ logits_processors=[SlidingWindowNoRepeatNGramProcessor(35, 128)],
130
+ verbose=False,
131
+ )
132
+
133
+ if isinstance(response, str):
134
+ return response
135
+ elif hasattr(response, "text"):
136
+ return response.text
137
+ return str(response)
138
+
139
+
140
+ def evaluate_model(
141
+ model,
142
+ processor,
143
+ eval_dir: Path,
144
+ prompt: str = "<image>document parsing.",
145
+ max_tokens: int = 4096,
146
+ ) -> dict:
147
+ """Evaluate model on a directory of images with ground truth.
148
+
149
+ Expects eval_dir to contain:
150
+ - images/ subdirectory with input images
151
+ - ground_truth/ subdirectory with .txt files (same stem as image)
152
+ """
153
+ evaluation_files = collect_evaluation_files(eval_dir, prompt, max_tokens)
154
+
155
+ cer_scores = []
156
+ digit_cer_scores = []
157
+ table_scores = []
158
+ total_time = 0.0
159
+
160
+ for img_file, gt_file in evaluation_files:
161
+ reference = normalize_ocr_output(gt_file.read_text(encoding="utf-8"))
162
+
163
+ start = time.perf_counter()
164
+ hypothesis = normalize_ocr_output(run_inference(
165
+ model,
166
+ processor,
167
+ str(img_file),
168
+ prompt,
169
+ max_tokens=max_tokens,
170
+ ))
171
+ elapsed = time.perf_counter() - start
172
+
173
+ cer = compute_cer(reference, hypothesis)
174
+ digit_cer = compute_digit_cer(reference, hypothesis)
175
+
176
+ cer_scores.append(cer)
177
+ if any(char.isascii() and char.isdigit() for char in reference):
178
+ digit_cer_scores.append(digit_cer)
179
+ table_result = evaluate_tables(reference, hypothesis)
180
+ if table_result["num_ref_tables"] > 0:
181
+ table_scores.append(table_result["mean_score"])
182
+ total_time += elapsed
183
+
184
+ return {
185
+ "num_samples": len(cer_scores),
186
+ "num_digit_samples": len(digit_cer_scores),
187
+ "num_table_samples": len(table_scores),
188
+ "mean_cer": float(np.mean(cer_scores)) if cer_scores else 0.0,
189
+ "mean_digit_cer": float(np.mean(digit_cer_scores)) if digit_cer_scores else None,
190
+ "mean_table_score": float(np.mean(table_scores)) if table_scores else None,
191
+ "total_inference_time": total_time,
192
+ "avg_time_per_image": total_time / max(len(cer_scores), 1),
193
+ }
194
+
195
+
196
+ def quantize_layer_group(model, config: dict, group_name: str) -> list[str]:
197
+ """Quantize one configured MLX module group to MXFP8 in place.
198
+
199
+ Returns the concrete module paths that were converted.
200
+ """
201
+ if group_name not in LAYER_GROUPS:
202
+ available = ", ".join(LAYER_GROUPS)
203
+ raise ValueError(f"Unknown layer group '{group_name}'. Available: {available}")
204
+
205
+ from mlx_vlm.quant_utils import quantize_model
206
+
207
+ patterns = LAYER_GROUPS[group_name]
208
+ matched: list[str] = []
209
+
210
+ def predicate(path, module):
211
+ if any(matches_pattern(path, pattern) for pattern in patterns):
212
+ matched.append(path)
213
+ return {"group_size": 32, "bits": 8, "mode": "mxfp8"}
214
+ return False
215
+
216
+ quantize_model(
217
+ model,
218
+ config,
219
+ group_size=32,
220
+ bits=8,
221
+ mode="mxfp8",
222
+ quant_predicate=predicate,
223
+ )
224
+ if not matched:
225
+ raise RuntimeError(
226
+ f"Layer group '{group_name}' matched no quantizable MLX modules"
227
+ )
228
+ return sorted(set(matched))
229
+
230
+
231
+ def main():
232
+ parser = argparse.ArgumentParser(
233
+ description="Layer sensitivity analysis for OCR-aware quantization"
234
+ )
235
+ parser.add_argument("--model-path", required=True,
236
+ help="Path to BF16 model (baidu/Unlimited-OCR or local)")
237
+ parser.add_argument(
238
+ "--source-id",
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"),
245
+ help="Output JSON file for results")
246
+ parser.add_argument("--prompt", default="<image>document parsing.",
247
+ help="OCR prompt to use")
248
+ parser.add_argument("--max-tokens", type=int, default=4096)
249
+ parser.add_argument(
250
+ "--groups",
251
+ nargs="+",
252
+ choices=list(LAYER_GROUPS),
253
+ default=list(LAYER_GROUPS),
254
+ help="Layer groups to test (default: all)",
255
+ )
256
+ args = parser.parse_args()
257
+ if args.max_tokens < 1:
258
+ parser.error("--max-tokens must be positive")
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")
265
+ print("=" * 60)
266
+
267
+ # Step 1: Load BF16 baseline
268
+ print("\n[1/3] Loading BF16 baseline model...")
269
+ from mlx_vlm import load
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...")
282
+ baseline = evaluate_model(
283
+ model,
284
+ processor,
285
+ args.eval_dir,
286
+ args.prompt,
287
+ max_tokens=args.max_tokens,
288
+ )
289
+ print(f" Baseline CER: {baseline['mean_cer']:.4f}")
290
+ digit_label = (
291
+ f"{baseline['mean_digit_cer']:.4f}"
292
+ if baseline["mean_digit_cer"] is not None
293
+ else "N/A"
294
+ )
295
+ print(f" Baseline Digit CER: {digit_label}")
296
+ print(f" Samples: {baseline['num_samples']}")
297
+ model = None
298
+ processor = None
299
+ gc.collect()
300
+ mx.clear_cache()
301
+
302
+ # Step 3: Per-group sensitivity
303
+ print("\n[3/3] Testing per-group quantization sensitivity...")
304
+ results = {
305
+ "baseline": baseline,
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,
313
+ "layer_groups": {},
314
+ }
315
+
316
+ for group in args.groups:
317
+ print(f"\n Testing: {group}")
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),
325
+ group,
326
+ )
327
+ metrics = evaluate_model(
328
+ group_model,
329
+ group_processor,
330
+ args.eval_dir,
331
+ args.prompt,
332
+ max_tokens=args.max_tokens,
333
+ )
334
+ digit_delta = (
335
+ metrics["mean_digit_cer"] - baseline["mean_digit_cer"]
336
+ if metrics["mean_digit_cer"] is not None
337
+ and baseline["mean_digit_cer"] is not None
338
+ else None
339
+ )
340
+ table_delta = (
341
+ baseline["mean_table_score"] - metrics["mean_table_score"]
342
+ if metrics["mean_table_score"] is not None
343
+ and baseline["mean_table_score"] is not None
344
+ else None
345
+ )
346
+ results["layer_groups"][group] = {
347
+ "status": "success",
348
+ "matched_module_count": len(matched_modules),
349
+ "matched_modules": matched_modules,
350
+ "metrics": metrics,
351
+ "cer_delta": metrics["mean_cer"] - baseline["mean_cer"],
352
+ "digit_cer_delta": digit_delta,
353
+ "table_score_degradation": table_delta,
354
+ "sensitivity_rank": None,
355
+ }
356
+ print(
357
+ f" CER={metrics['mean_cer']:.4f} "
358
+ f"(delta={metrics['mean_cer'] - baseline['mean_cer']:+.4f})"
359
+ )
360
+ except Exception as exc:
361
+ results["layer_groups"][group] = {
362
+ "status": "error",
363
+ "error": str(exc),
364
+ "sensitivity_rank": None,
365
+ }
366
+ print(f" ERROR: {exc}")
367
+ finally:
368
+ group_model = None
369
+ group_processor = None
370
+ gc.collect()
371
+ mx.clear_cache()
372
+
373
+ successful_groups = [
374
+ (name, data)
375
+ for name, data in results["layer_groups"].items()
376
+ if data["status"] == "success"
377
+ ]
378
+ successful_groups.sort(
379
+ key=lambda item: (
380
+ item[1]["cer_delta"],
381
+ item[1]["digit_cer_delta"]
382
+ if item[1]["digit_cer_delta"] is not None
383
+ else float("-inf"),
384
+ ),
385
+ reverse=True,
386
+ )
387
+ for rank, (_, data) in enumerate(successful_groups, start=1):
388
+ data["sensitivity_rank"] = rank
389
+
390
+ # Save results
391
+ args.output.parent.mkdir(parents=True, exist_ok=True)
392
+ args.output.write_text(
393
+ json.dumps(results, indent=2, ensure_ascii=False),
394
+ encoding="utf-8",
395
+ )
396
+ print(f"\nResults saved to: {args.output}")
397
+ if any(data["status"] == "error" for data in results["layer_groups"].values()):
398
+ raise SystemExit(1)
399
+
400
+
401
+ if __name__ == "__main__":
402
+ main()
quantization/mixed_precision_convert.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mixed-precision conversion script for OCR-aware quantization.
2
+
3
+ Applies a precision map to produce enhanced model weights where OCR-sensitive
4
+ layers remain at BF16 while less sensitive layers use MXFP8.
5
+
6
+ Usage:
7
+ python quantization/mixed_precision_convert.py \
8
+ --model-path baidu/Unlimited-OCR \
9
+ --precision-map quantization/precision_map.json \
10
+ --output-dir ./enhanced_model/
11
+
12
+ Requires: mlx, mlx-vlm
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import hashlib
19
+ import json
20
+ import re
21
+ import shutil
22
+ import tempfile
23
+ from pathlib import Path
24
+
25
+
26
+ SUPPORTED_PRECISIONS = {"bfloat16", "mxfp8", "affine8"}
27
+ QUANTIZATION_CONFIGS = {
28
+ "mxfp8": {"group_size": 32, "bits": 8, "mode": "mxfp8"},
29
+ "affine8": {"group_size": 32, "bits": 8, "mode": "affine"},
30
+ }
31
+
32
+
33
+ def _json_digest(value: dict) -> str:
34
+ payload = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
35
+ return hashlib.sha256(payload).hexdigest()
36
+
37
+
38
+ def normalize_unlimited_ocr_metadata(model_dir: Path) -> None:
39
+ """Select mlx-vlm's native backend and validate MXFP8/R-SWA metadata.
40
+
41
+ ``mlx-vlm convert`` always emits ``config.json`` (and usually
42
+ ``processor_config.json``). Missing configs mean conversion did not finish
43
+ and must not be published.
44
+ """
45
+ config_path = model_dir / "config.json"
46
+ processor_path = model_dir / "processor_config.json"
47
+ if not config_path.is_file():
48
+ raise ValueError(
49
+ "Converted model is missing config.json — mlx-vlm conversion incomplete"
50
+ )
51
+ try:
52
+ config = json.loads(config_path.read_text(encoding="utf-8"))
53
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
54
+ raise ValueError("Converted model has invalid config.json") from exc
55
+ if not isinstance(config, dict):
56
+ raise ValueError("Converted config.json must contain a JSON object")
57
+
58
+ architectures = config.get("architectures")
59
+ if not isinstance(architectures, list) or "UnlimitedOCRForCausalLM" not in architectures:
60
+ raise ValueError("Converted checkpoint is not UnlimitedOCRForCausalLM")
61
+ quantization = config.get("quantization") or config.get("quantization_config")
62
+ if not isinstance(quantization, dict) or quantization.get("mode") != "mxfp8":
63
+ raise ValueError("Converted checkpoint does not declare MXFP8 quantization")
64
+ text_config = config.get("language_config") or config.get("text_config") or config
65
+ window_size = None
66
+ if isinstance(text_config, dict):
67
+ window_size = text_config.get("sliding_window_size", text_config.get("sliding_window"))
68
+ if window_size is None:
69
+ window_size = config.get("sliding_window_size", config.get("sliding_window"))
70
+ if not isinstance(window_size, int) or isinstance(window_size, bool) or window_size < 1:
71
+ raise ValueError("Converted checkpoint is missing a positive sliding-window size")
72
+
73
+ config["model_type"] = "unlimited-ocr"
74
+ config.pop("auto_map", None)
75
+ # Keep R-SWA fields consistent at the top level and under language_config.
76
+ config["sliding_window"] = int(window_size)
77
+ config["sliding_window_size"] = int(window_size)
78
+ language_config = config.get("language_config")
79
+ if isinstance(language_config, dict):
80
+ language_config.pop("auto_map", None)
81
+ language_config["sliding_window"] = int(window_size)
82
+ language_config["sliding_window_size"] = int(window_size)
83
+
84
+ if processor_path.is_file():
85
+ try:
86
+ processor = json.loads(processor_path.read_text(encoding="utf-8"))
87
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
88
+ raise ValueError("Converted model has invalid processor_config.json") from exc
89
+ if not isinstance(processor, dict):
90
+ raise ValueError("Converted processor_config.json must contain a JSON object")
91
+ else:
92
+ # Some convert paths only emit tokenizer assets; still mark the processor
93
+ # class so mlx-vlm loads the Unlimited-OCR handler.
94
+ processor = {}
95
+
96
+ processor["processor_class"] = "UnlimitedOCRHFProcessor"
97
+ processor["sft_format"] = "unlimitedocr"
98
+ config_path.write_text(
99
+ json.dumps(config, indent=2, ensure_ascii=False) + "\n",
100
+ encoding="utf-8",
101
+ )
102
+ processor_path.write_text(
103
+ json.dumps(processor, indent=2, ensure_ascii=False) + "\n",
104
+ encoding="utf-8",
105
+ )
106
+
107
+
108
+ def load_precision_map(path: Path) -> dict:
109
+ """Load the precision map JSON."""
110
+ with open(path, encoding="utf-8") as f:
111
+ precision_map = json.load(f)
112
+ validate_precision_map(precision_map)
113
+ return precision_map
114
+
115
+
116
+ def validate_precision_map(precision_map: dict) -> None:
117
+ """Validate precision-map structure and supported precision values."""
118
+ if not isinstance(precision_map, dict):
119
+ raise ValueError("precision map must be a JSON object")
120
+ rules = {
121
+ pattern: precision
122
+ for pattern, precision in precision_map.items()
123
+ if not pattern.startswith("_")
124
+ }
125
+ if not rules:
126
+ raise ValueError("precision map has no module rules")
127
+ invalid = {
128
+ pattern: precision
129
+ for pattern, precision in rules.items()
130
+ if not isinstance(precision, str) or precision not in SUPPORTED_PRECISIONS
131
+ }
132
+ if invalid:
133
+ details = ", ".join(f"{pattern}={precision!r}" for pattern, precision in invalid.items())
134
+ raise ValueError(f"unsupported precision-map values: {details}")
135
+ if "mxfp8" not in rules.values():
136
+ raise ValueError("precision map does not select any modules for MXFP8")
137
+
138
+
139
+ def matches_pattern(param_name: str, pattern: str) -> bool:
140
+ """Check if a parameter name matches a precision map pattern.
141
+
142
+ Supports wildcard '*' for layer indices.
143
+ Example: ``model.layers.*.self_attn.q_proj`` matches layer 5's q_proj.
144
+ """
145
+ regex = re.escape(pattern).replace(r"\*", r"\d+")
146
+ # Precision-map entries name complete module-path segments but parameter
147
+ # names may contain a model prefix and a trailing ``.weight``.
148
+ return bool(re.search(rf"(?:^|\.){regex}(?=\.|$)", param_name))
149
+
150
+
151
+ def get_precision_for_param(param_name: str, precision_map: dict) -> str:
152
+ """Determine the target precision for a given parameter name."""
153
+ matching_rules = [
154
+ (index, pattern, precision)
155
+ for index, (pattern, precision) in enumerate(precision_map.items())
156
+ if not pattern.startswith("_") and matches_pattern(param_name, pattern)
157
+ ]
158
+ if matching_rules:
159
+ # Exact per-layer sensitivity overrides must beat an earlier wildcard
160
+ # group rule. More literal path segments are more specific; insertion
161
+ # order is only a tie breaker.
162
+ _, _, precision = max(
163
+ matching_rules,
164
+ key=lambda item: (
165
+ len([segment for segment in item[1].split(".") if segment != "*"]),
166
+ len(item[1].split(".")),
167
+ item[0],
168
+ ),
169
+ )
170
+ return precision
171
+ # Unlisted modules are preserved. Quantizing by a loose name heuristic can
172
+ # accidentally include the vision tower or projector.
173
+ return "bfloat16"
174
+
175
+
176
+ def convert_model(
177
+ model_path: str,
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
+
184
+ The conversion is staged in a temporary sibling directory and moved into
185
+ place only after quantized weights and all requested rule matches exist.
186
+ """
187
+ validate_precision_map(precision_map)
188
+ output_dir = Path(output_dir)
189
+ if output_dir.exists():
190
+ raise FileExistsError(
191
+ f"Output directory already exists; choose a new path: {output_dir}"
192
+ )
193
+ output_dir.parent.mkdir(parents=True, exist_ok=True)
194
+
195
+ from mlx_vlm.convert import convert
196
+
197
+ if verbose:
198
+ print(f"Loading and converting model from: {model_path}")
199
+
200
+ quantized_modules: list[str] = []
201
+ quantized_precisions: dict[str, str] = {}
202
+ preserved_modules: list[str] = []
203
+
204
+ def quantization_predicate(path, module):
205
+ precision = get_precision_for_param(path, precision_map)
206
+ if precision in QUANTIZATION_CONFIGS:
207
+ quantized_modules.append(path)
208
+ quantized_precisions[path] = precision
209
+ return dict(QUANTIZATION_CONFIGS[precision])
210
+ preserved_modules.append(path)
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}")
218
+
219
+ staging_dir = Path(tempfile.mkdtemp(
220
+ prefix=f".{output_dir.name}_staging_",
221
+ dir=output_dir.parent,
222
+ ))
223
+ try:
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,
230
+ q_mode="mxfp8",
231
+ quant_predicate=quantization_predicate,
232
+ )
233
+
234
+ if not quantized_modules:
235
+ raise RuntimeError(
236
+ "Precision map matched no quantizable MLX modules; refusing to save a BF16-only model"
237
+ )
238
+ requested_patterns = [
239
+ pattern
240
+ for pattern, precision in precision_map.items()
241
+ if not pattern.startswith("_") and precision in QUANTIZATION_CONFIGS
242
+ ]
243
+ unmatched_patterns = [
244
+ pattern
245
+ for pattern in requested_patterns
246
+ if not any(matches_pattern(path, pattern) for path in quantized_modules)
247
+ ]
248
+ if unmatched_patterns:
249
+ raise RuntimeError(
250
+ "MXFP8 precision rules matched no modules: " + ", ".join(unmatched_patterns)
251
+ )
252
+ if not list(staging_dir.glob("*.safetensors")):
253
+ raise RuntimeError("mlx-vlm conversion produced no safetensors weights")
254
+
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(
262
+ json.dumps({
263
+ "method": "mxfp8",
264
+ "group_size": 32,
265
+ "bits": 8,
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": {
272
+ precision: sum(
273
+ selected == precision
274
+ for selected in quantized_precisions.values()
275
+ )
276
+ for precision in sorted(set(quantized_precisions.values()))
277
+ },
278
+ "preserved_quantizable_module_count": len(set(preserved_modules)),
279
+ "quantized_modules": sorted(set(quantized_modules)),
280
+ "quantized_module_precisions": dict(sorted(quantized_precisions.items())),
281
+ }, indent=2),
282
+ encoding="utf-8",
283
+ )
284
+ staging_dir.replace(output_dir)
285
+ finally:
286
+ if staging_dir.exists():
287
+ shutil.rmtree(staging_dir)
288
+
289
+ if verbose:
290
+ print(f"\nQuantized {len(set(quantized_modules))} module(s).")
291
+ print(f"Model saved to: {output_dir}")
292
+ return output_dir
293
+
294
+
295
+ def main():
296
+ parser = argparse.ArgumentParser(
297
+ description="Apply OCR-aware mixed-precision quantization"
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")
304
+ parser.add_argument("--output-dir", type=Path, default=Path("./enhanced_model"),
305
+ help="Output directory for enhanced model")
306
+ parser.add_argument(
307
+ "--verbose",
308
+ action=argparse.BooleanOptionalAction,
309
+ default=True,
310
+ help="Show conversion progress (use --no-verbose to suppress)",
311
+ )
312
+ args = parser.parse_args()
313
+
314
+ print("=" * 60)
315
+ print("Unlimited-OCR Mixed-Precision Conversion")
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__":
323
+ main()
quantization/precision_map.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vision_model": "bfloat16",
3
+ "projector": "bfloat16",
4
+ "language_model.model.embed_tokens": "bfloat16",
5
+ "language_model.model.norm": "bfloat16",
6
+ "language_model.lm_head": "bfloat16",
7
+ "language_model.model.layers.*.input_layernorm": "bfloat16",
8
+ "language_model.model.layers.*.post_attention_layernorm": "bfloat16",
9
+ "language_model.model.layers.*.self_attn.q_proj": "mxfp8",
10
+ "language_model.model.layers.*.self_attn.k_proj": "mxfp8",
11
+ "language_model.model.layers.*.self_attn.v_proj": "mxfp8",
12
+ "language_model.model.layers.*.self_attn.o_proj": "mxfp8",
13
+ "language_model.model.layers.*.mlp.gate_proj": "mxfp8",
14
+ "language_model.model.layers.*.mlp.up_proj": "mxfp8",
15
+ "language_model.model.layers.*.mlp.down_proj": "mxfp8",
16
+ "language_model.model.layers.*.mlp.shared_experts.gate_proj": "mxfp8",
17
+ "language_model.model.layers.*.mlp.shared_experts.up_proj": "mxfp8",
18
+ "language_model.model.layers.*.mlp.shared_experts.down_proj": "mxfp8",
19
+ "language_model.model.layers.*.mlp.switch_mlp.gate_proj": "mxfp8",
20
+ "language_model.model.layers.*.mlp.switch_mlp.up_proj": "mxfp8",
21
+ "language_model.model.layers.*.mlp.switch_mlp.down_proj": "mxfp8",
22
+ "language_model.model.layers.*.mlp.gate": "bfloat16",
23
+ "_comment": "MLX module paths for DeepseekOCR. Refine via layer_sensitivity.py before final release.",
24
+ "_sensitivity_override": {
25
+ "_description": "Layers identified as OCR-sensitive by sensitivity analysis should be promoted to bfloat16. Run layer_sensitivity.py to populate this section.",
26
+ "examples": [
27
+ "language_model.model.layers.11.self_attn.q_proj -> bfloat16 (if digit CER degrades >2%)",
28
+ "language_model.model.layers.1.mlp.switch_mlp -> bfloat16 (if table score degrades >1%)"
29
+ ]
30
+ }
31
+ }
quantization/release_gate.py ADDED
@@ -0,0 +1,584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Create a content-addressed, fail-closed model release manifest."""
3
+
4
+ from __future__ import annotations
5
+
6
+ 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):
47
+ digest.update(chunk)
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
+
58
+ aggregate = hashlib.sha256()
59
+ files = []
60
+ total_size = 0
61
+ for path in weight_paths:
62
+ size = path.stat().st_size
63
+ digest = sha256_file(path)
64
+ total_size += size
65
+ aggregate.update(path.name.encode("utf-8"))
66
+ aggregate.update(b"\0")
67
+ aggregate.update(str(size).encode("ascii"))
68
+ aggregate.update(b"\0")
69
+ aggregate.update(digest.encode("ascii"))
70
+ aggregate.update(b"\n")
71
+ files.append({"name": path.name, "size": size, "sha256": digest})
72
+
73
+ return {
74
+ "files": files,
75
+ "total_size_bytes": total_size,
76
+ "total_size_gb": total_size / (1024**3),
77
+ "aggregate_sha256": aggregate.hexdigest(),
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 = []
115
+ category_counts: Counter[str] = Counter()
116
+ digit_samples = 0
117
+ cjk_samples = 0
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()
143
+ digest = sha256_file(path)
144
+ size = path.stat().st_size
145
+ content_digest.update(relative.encode("utf-8"))
146
+ content_digest.update(b"\0")
147
+ content_digest.update(digest.encode("ascii"))
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,
160
+ "num_samples": len(image_paths),
161
+ "num_digit_samples": digit_samples,
162
+ "num_cjk_samples": cjk_samples,
163
+ "num_table_samples": table_samples,
164
+ "category_counts": dict(sorted(category_counts.items())),
165
+ "manifest_sha256": sha256_file(manifest_path),
166
+ "content_sha256": content_digest.hexdigest(),
167
+ "files": files,
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,
179
+ *,
180
+ calibration_results: dict | None = None,
181
+ cer_threshold: float = 0.02,
182
+ digit_cer_threshold: float = 0.02,
183
+ table_degradation_threshold: float = 0.01,
184
+ ) -> dict:
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")
191
+ generated = {
192
+ pattern: precision
193
+ for pattern, precision in base_map.items()
194
+ if not pattern.startswith("_")
195
+ }
196
+ decisions = {}
197
+ for group_name, patterns in LAYER_GROUPS.items():
198
+ result = groups.get(group_name)
199
+ if not isinstance(result, dict):
200
+ raise ValueError(f"Sensitivity result missing group: {group_name}")
201
+ reasons = []
202
+ if result.get("status") != "success":
203
+ reasons.append(f"status={result.get('status', 'missing')}")
204
+ for key, threshold in (
205
+ ("cer_delta", cer_threshold),
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:
236
+ for group_pattern in patterns:
237
+ for existing_pattern in list(generated):
238
+ if _pattern_is_within(existing_pattern, group_pattern):
239
+ generated[existing_pattern] = "bfloat16"
240
+ generated[group_pattern] = "bfloat16"
241
+ elif group_name in EVIDENCE_PROTECTED_GROUPS and explicit_non_degradation:
242
+ for group_pattern in patterns:
243
+ for existing_pattern in list(generated):
244
+ if _pattern_is_within(existing_pattern, group_pattern):
245
+ generated[existing_pattern] = "mxfp8"
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"]
257
+ ),
258
+ }
259
+
260
+ calibration_summary = None
261
+ if calibration_results is not None:
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:
272
+ decisions[group_name] = {
273
+ "precision": precision,
274
+ "reasons": [
275
+ "selected by joint quality/throughput calibration: "
276
+ + str(calibration_results.get("selected", {}).get("label"))
277
+ ],
278
+ }
279
+ calibration_summary = {
280
+ "selected": calibration_results.get("selected"),
281
+ "selection_policy": calibration_results.get("selection_policy"),
282
+ }
283
+
284
+ generated["_generated_from"] = {
285
+ "source_model": sensitivity_results.get("model_path"),
286
+ "thresholds": {
287
+ "cer_delta": cer_threshold,
288
+ "digit_cer_delta": digit_cer_threshold,
289
+ "table_score_degradation": table_degradation_threshold,
290
+ },
291
+ "decisions": decisions,
292
+ "calibration": calibration_summary,
293
+ }
294
+ return generated
295
+
296
+
297
+ def validate_candidate_metadata(model_dir: str | Path) -> dict:
298
+ """Validate native Unlimited-OCR MXFP8 metadata."""
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(
329
+ *,
330
+ candidate_weights: dict,
331
+ reference_weights: dict,
332
+ dataset: dict,
333
+ metadata: dict,
334
+ bf16_accuracy: dict,
335
+ reference_accuracy: dict,
336
+ candidate_accuracy: dict,
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),
365
+ "digit": dataset.get("num_digit_samples", 0),
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
+
443
+
444
+ def _runtime_versions() -> dict:
445
+ versions = {}
446
+ for distribution in ("mlx", "mlx-vlm", "huggingface-hub", "numpy", "Pillow"):
447
+ try:
448
+ versions[distribution] = importlib.metadata.version(distribution)
449
+ except importlib.metadata.PackageNotFoundError:
450
+ versions[distribution] = None
451
+ return versions
452
+
453
+
454
+ def build_release_manifest(
455
+ *,
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"],
489
+ candidate_accuracy=evidence["candidate_accuracy"],
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,
498
+ "size": Path(path).stat().st_size,
499
+ "sha256": sha256_file(path),
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"],
526
+ "sensitivity": {
527
+ "baseline": evidence["sensitivity_results"].get("baseline"),
528
+ "groups": evidence["sensitivity_results"].get("layer_groups"),
529
+ },
530
+ "calibration": evidence["calibration_results"],
531
+ "precision_map": evidence["generated_precision_map"],
532
+ "artifacts": artifact_hashes,
533
+ "gates": gates,
534
+ "environment": {
535
+ "platform": platform.platform(),
536
+ "machine": platform.machine(),
537
+ "python": platform.python_version(),
538
+ "versions": _runtime_versions(),
539
+ },
540
+ }
541
+
542
+
543
+ def main() -> None:
544
+ parser = argparse.ArgumentParser(description="Build a fail-closed release manifest")
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)
581
+
582
+
583
+ if __name__ == "__main__":
584
+ main()
quantization/run_pipeline.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Orchestrate verified OCR-aware quantization, validation, and publication."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib.metadata
8
+ import json
9
+ import platform
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+
17
+ QUANT_DIR = Path(__file__).resolve().parent
18
+ PROJECT_ROOT = QUANT_DIR.parent
19
+ 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
+
30
+ DEFAULT_SOURCE = PROJECT_ROOT / "reference" / "Unlimited-OCR"
31
+ DEFAULT_SOURCE_ID = "baidu/Unlimited-OCR"
32
+ DEFAULT_REFERENCE = "sahilchachra/unlimited-ocr-mxfp8-mlx"
33
+ DEFAULT_OUTPUT = PROJECT_ROOT / "models" / "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
34
+ DEFAULT_ARTIFACTS = PROJECT_ROOT / "artifacts" / "ocr-aware-v1"
35
+ DEFAULT_REPO = "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
36
+ BASE_PRECISION_MAP = QUANT_DIR / "precision_map.json"
37
+
38
+
39
+ def run_command(cmd: list[str], description: str, *, dry_run: bool = False) -> bool:
40
+ """Run one subprocess and stop the pipeline on any non-zero result."""
41
+ print("\n" + "=" * 72)
42
+ print(f"STEP: {description}")
43
+ print("CMD: " + " ".join(str(part) for part in cmd))
44
+ print("=" * 72)
45
+ if dry_run:
46
+ print("[DRY RUN] Command not executed")
47
+ return True
48
+ started = time.perf_counter()
49
+ result = subprocess.run(cmd, cwd=str(PROJECT_ROOT))
50
+ elapsed = time.perf_counter() - started
51
+ print(f"[{'OK' if result.returncode == 0 else 'FAIL'}] {description} ({elapsed:.1f}s)")
52
+ return result.returncode == 0
53
+
54
+
55
+ def _config_path(model_path: str) -> Path:
56
+ local = Path(model_path)
57
+ if local.is_dir():
58
+ return local / "config.json"
59
+ from huggingface_hub import hf_hub_download
60
+ return Path(hf_hub_download(model_path, filename="config.json"))
61
+
62
+
63
+ def assert_unquantized_source(model_path: str) -> dict:
64
+ """Reject an already quantized source before expensive work."""
65
+ config_path = _config_path(model_path)
66
+ config = load_json_object(config_path)
67
+ if config.get("quantization") or config.get("quantization_config"):
68
+ raise ValueError("Source is already quantized; use the upstream BF16 checkpoint")
69
+ architectures = config.get("architectures")
70
+ if not isinstance(architectures, list) or "UnlimitedOCRForCausalLM" not in architectures:
71
+ raise ValueError("Source is not an UnlimitedOCRForCausalLM checkpoint")
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"):
78
+ try:
79
+ versions[distribution] = importlib.metadata.version(distribution)
80
+ except importlib.metadata.PackageNotFoundError:
81
+ versions[distribution] = None
82
+ return {
83
+ "created_at": datetime.now(timezone.utc).isoformat(),
84
+ "python": platform.python_version(),
85
+ "platform": platform.platform(),
86
+ "machine": platform.machine(),
87
+ "processor": platform.processor(),
88
+ "versions": versions,
89
+ }
90
+
91
+
92
+ 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"}:
100
+ raise FileExistsError(f"Output model directory already exists: {args.output_dir}")
101
+ args.artifacts_dir.mkdir(parents=True, exist_ok=True)
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),
113
+ },
114
+ "parameters": {
115
+ "accuracy_tokens": args.accuracy_tokens,
116
+ "performance_tokens": args.performance_tokens,
117
+ "performance_warmup": args.performance_warmup,
118
+ "performance_runs": args.performance_runs,
119
+ "rswa_lengths": args.rswa_lengths,
120
+ },
121
+ }
122
+ (args.artifacts_dir / "provenance.json").write_text(
123
+ json.dumps(provenance, indent=2, ensure_ascii=False) + "\n",
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}")
132
+ return False
133
+
134
+
135
+ def sensitivity(args: argparse.Namespace) -> bool:
136
+ cmd = [
137
+ sys.executable,
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"
151
+ if args.dry_run:
152
+ print(f"[DRY RUN] Generate {output_path} from {sensitivity_path}")
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",
167
+ encoding="utf-8",
168
+ )
169
+ print(f"[OK] Generated executable precision map: {output_path}")
170
+ return True
171
+ except Exception as exc:
172
+ print(f"[FAIL] Precision-map generation: {exc}")
173
+ return False
174
+
175
+
176
+ def convert(args: argparse.Namespace) -> bool:
177
+ cmd = [
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,
192
+ "--eval-dir", str(args.eval_dir),
193
+ "--output", str(output),
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,
204
+ "--image", str(args.image),
205
+ "--output", str(output),
206
+ "--max-tokens", str(args.performance_tokens),
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,
222
+ str(PROJECT_ROOT / "benchmarks" / "rswa_validation.py"),
223
+ "--model-path", str(args.output_dir),
224
+ "--image", str(args.image),
225
+ "--output", str(args.artifacts_dir / "candidate_rswa.json"),
226
+ "--lengths", *[str(length) for length in args.rswa_lengths],
227
+ "--force-min-tokens", str(max(args.rswa_lengths)),
228
+ ], "Candidate R-SWA stress validation"),
229
+ ]
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",
243
+ "chat_template.jinja",
244
+ ],
245
+ ))
246
+
247
+
248
+ def gate(args: argparse.Namespace) -> bool:
249
+ if args.dry_run:
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
257
+ cmd = [
258
+ sys.executable,
259
+ str(QUANT_DIR / "release_gate.py"),
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
+
272
+
273
+ def publish(args: argparse.Namespace) -> bool:
274
+ cmd = [
275
+ sys.executable,
276
+ str(PROJECT_ROOT / "scripts" / "publish_optimized_model.py"),
277
+ "--model-dir", str(args.output_dir),
278
+ "--manifest", str(args.artifacts_dir / "release_manifest.json"),
279
+ "--artifacts-dir", str(args.artifacts_dir),
280
+ "--repo-id", args.repo_id,
281
+ ]
282
+ if args.dry_run:
283
+ cmd.append("--dry-run")
284
+ return run_command(cmd, "Publish approved candidate to Hugging Face")
285
+
286
+
287
+ 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")
296
+ parser.add_argument("--repo-id", default=DEFAULT_REPO)
297
+ parser.add_argument("--accuracy-tokens", type=int, default=2048)
298
+ parser.add_argument("--performance-tokens", type=int, default=256)
299
+ parser.add_argument("--performance-warmup", type=int, default=1)
300
+ parser.add_argument("--performance-runs", type=int, default=3)
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
318
+
319
+
320
+ def main() -> None:
321
+ args = parse_args()
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]():
334
+ print(f"[ABORT] Pipeline stopped at: {step_name}")
335
+ raise SystemExit(1)
336
+ print("[DONE] Requested pipeline steps completed successfully")
337
+
338
+
339
+ if __name__ == "__main__":
340
+ main()
quantization_config.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
quantization_summary.json ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
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",
16
+ "language_model.model.layers.0.mlp.down_proj",
17
+ "language_model.model.layers.0.mlp.gate_proj",
18
+ "language_model.model.layers.0.mlp.up_proj",
19
+ "language_model.model.layers.0.self_attn.k_proj",
20
+ "language_model.model.layers.0.self_attn.o_proj",
21
+ "language_model.model.layers.0.self_attn.q_proj",
22
+ "language_model.model.layers.0.self_attn.v_proj",
23
+ "language_model.model.layers.1.mlp.shared_experts.down_proj",
24
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj",
25
+ "language_model.model.layers.1.mlp.shared_experts.up_proj",
26
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj",
27
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj",
28
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj",
29
+ "language_model.model.layers.1.self_attn.k_proj",
30
+ "language_model.model.layers.1.self_attn.o_proj",
31
+ "language_model.model.layers.1.self_attn.q_proj",
32
+ "language_model.model.layers.1.self_attn.v_proj",
33
+ "language_model.model.layers.10.mlp.shared_experts.down_proj",
34
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj",
35
+ "language_model.model.layers.10.mlp.shared_experts.up_proj",
36
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj",
37
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj",
38
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj",
39
+ "language_model.model.layers.10.self_attn.k_proj",
40
+ "language_model.model.layers.10.self_attn.o_proj",
41
+ "language_model.model.layers.10.self_attn.q_proj",
42
+ "language_model.model.layers.10.self_attn.v_proj",
43
+ "language_model.model.layers.11.mlp.shared_experts.down_proj",
44
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj",
45
+ "language_model.model.layers.11.mlp.shared_experts.up_proj",
46
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj",
47
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj",
48
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj",
49
+ "language_model.model.layers.11.self_attn.k_proj",
50
+ "language_model.model.layers.11.self_attn.o_proj",
51
+ "language_model.model.layers.11.self_attn.q_proj",
52
+ "language_model.model.layers.11.self_attn.v_proj",
53
+ "language_model.model.layers.2.mlp.shared_experts.down_proj",
54
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj",
55
+ "language_model.model.layers.2.mlp.shared_experts.up_proj",
56
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj",
57
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj",
58
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj",
59
+ "language_model.model.layers.2.self_attn.k_proj",
60
+ "language_model.model.layers.2.self_attn.o_proj",
61
+ "language_model.model.layers.2.self_attn.q_proj",
62
+ "language_model.model.layers.2.self_attn.v_proj",
63
+ "language_model.model.layers.3.mlp.shared_experts.down_proj",
64
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj",
65
+ "language_model.model.layers.3.mlp.shared_experts.up_proj",
66
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj",
67
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj",
68
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj",
69
+ "language_model.model.layers.3.self_attn.k_proj",
70
+ "language_model.model.layers.3.self_attn.o_proj",
71
+ "language_model.model.layers.3.self_attn.q_proj",
72
+ "language_model.model.layers.3.self_attn.v_proj",
73
+ "language_model.model.layers.4.mlp.shared_experts.down_proj",
74
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj",
75
+ "language_model.model.layers.4.mlp.shared_experts.up_proj",
76
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj",
77
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj",
78
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj",
79
+ "language_model.model.layers.4.self_attn.k_proj",
80
+ "language_model.model.layers.4.self_attn.o_proj",
81
+ "language_model.model.layers.4.self_attn.q_proj",
82
+ "language_model.model.layers.4.self_attn.v_proj",
83
+ "language_model.model.layers.5.mlp.shared_experts.down_proj",
84
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj",
85
+ "language_model.model.layers.5.mlp.shared_experts.up_proj",
86
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj",
87
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj",
88
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj",
89
+ "language_model.model.layers.5.self_attn.k_proj",
90
+ "language_model.model.layers.5.self_attn.o_proj",
91
+ "language_model.model.layers.5.self_attn.q_proj",
92
+ "language_model.model.layers.5.self_attn.v_proj",
93
+ "language_model.model.layers.6.mlp.shared_experts.down_proj",
94
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj",
95
+ "language_model.model.layers.6.mlp.shared_experts.up_proj",
96
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj",
97
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj",
98
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj",
99
+ "language_model.model.layers.6.self_attn.k_proj",
100
+ "language_model.model.layers.6.self_attn.o_proj",
101
+ "language_model.model.layers.6.self_attn.q_proj",
102
+ "language_model.model.layers.6.self_attn.v_proj",
103
+ "language_model.model.layers.7.mlp.shared_experts.down_proj",
104
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj",
105
+ "language_model.model.layers.7.mlp.shared_experts.up_proj",
106
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj",
107
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj",
108
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj",
109
+ "language_model.model.layers.7.self_attn.k_proj",
110
+ "language_model.model.layers.7.self_attn.o_proj",
111
+ "language_model.model.layers.7.self_attn.q_proj",
112
+ "language_model.model.layers.7.self_attn.v_proj",
113
+ "language_model.model.layers.8.mlp.shared_experts.down_proj",
114
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj",
115
+ "language_model.model.layers.8.mlp.shared_experts.up_proj",
116
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj",
117
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj",
118
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj",
119
+ "language_model.model.layers.8.self_attn.k_proj",
120
+ "language_model.model.layers.8.self_attn.o_proj",
121
+ "language_model.model.layers.8.self_attn.q_proj",
122
+ "language_model.model.layers.8.self_attn.v_proj",
123
+ "language_model.model.layers.9.mlp.shared_experts.down_proj",
124
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj",
125
+ "language_model.model.layers.9.mlp.shared_experts.up_proj",
126
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj",
127
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj",
128
+ "language_model.model.layers.9.mlp.switch_mlp.up_proj",
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",
137
+ "language_model.model.embed_tokens": "mxfp8",
138
+ "language_model.model.layers.0.mlp.down_proj": "mxfp8",
139
+ "language_model.model.layers.0.mlp.gate_proj": "mxfp8",
140
+ "language_model.model.layers.0.mlp.up_proj": "mxfp8",
141
+ "language_model.model.layers.0.self_attn.k_proj": "mxfp8",
142
+ "language_model.model.layers.0.self_attn.o_proj": "mxfp8",
143
+ "language_model.model.layers.0.self_attn.q_proj": "mxfp8",
144
+ "language_model.model.layers.0.self_attn.v_proj": "mxfp8",
145
+ "language_model.model.layers.1.mlp.shared_experts.down_proj": "mxfp8",
146
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj": "mxfp8",
147
+ "language_model.model.layers.1.mlp.shared_experts.up_proj": "mxfp8",
148
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj": "mxfp8",
149
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj": "mxfp8",
150
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj": "mxfp8",
151
+ "language_model.model.layers.1.self_attn.k_proj": "mxfp8",
152
+ "language_model.model.layers.1.self_attn.o_proj": "mxfp8",
153
+ "language_model.model.layers.1.self_attn.q_proj": "mxfp8",
154
+ "language_model.model.layers.1.self_attn.v_proj": "mxfp8",
155
+ "language_model.model.layers.10.mlp.shared_experts.down_proj": "mxfp8",
156
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj": "mxfp8",
157
+ "language_model.model.layers.10.mlp.shared_experts.up_proj": "mxfp8",
158
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj": "mxfp8",
159
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj": "mxfp8",
160
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj": "mxfp8",
161
+ "language_model.model.layers.10.self_attn.k_proj": "mxfp8",
162
+ "language_model.model.layers.10.self_attn.o_proj": "mxfp8",
163
+ "language_model.model.layers.10.self_attn.q_proj": "mxfp8",
164
+ "language_model.model.layers.10.self_attn.v_proj": "mxfp8",
165
+ "language_model.model.layers.11.mlp.shared_experts.down_proj": "mxfp8",
166
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj": "mxfp8",
167
+ "language_model.model.layers.11.mlp.shared_experts.up_proj": "mxfp8",
168
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj": "mxfp8",
169
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj": "mxfp8",
170
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj": "mxfp8",
171
+ "language_model.model.layers.11.self_attn.k_proj": "mxfp8",
172
+ "language_model.model.layers.11.self_attn.o_proj": "mxfp8",
173
+ "language_model.model.layers.11.self_attn.q_proj": "mxfp8",
174
+ "language_model.model.layers.11.self_attn.v_proj": "mxfp8",
175
+ "language_model.model.layers.2.mlp.shared_experts.down_proj": "mxfp8",
176
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj": "mxfp8",
177
+ "language_model.model.layers.2.mlp.shared_experts.up_proj": "mxfp8",
178
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj": "mxfp8",
179
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj": "mxfp8",
180
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj": "mxfp8",
181
+ "language_model.model.layers.2.self_attn.k_proj": "mxfp8",
182
+ "language_model.model.layers.2.self_attn.o_proj": "mxfp8",
183
+ "language_model.model.layers.2.self_attn.q_proj": "mxfp8",
184
+ "language_model.model.layers.2.self_attn.v_proj": "mxfp8",
185
+ "language_model.model.layers.3.mlp.shared_experts.down_proj": "mxfp8",
186
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj": "mxfp8",
187
+ "language_model.model.layers.3.mlp.shared_experts.up_proj": "mxfp8",
188
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj": "mxfp8",
189
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj": "mxfp8",
190
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj": "mxfp8",
191
+ "language_model.model.layers.3.self_attn.k_proj": "mxfp8",
192
+ "language_model.model.layers.3.self_attn.o_proj": "mxfp8",
193
+ "language_model.model.layers.3.self_attn.q_proj": "mxfp8",
194
+ "language_model.model.layers.3.self_attn.v_proj": "mxfp8",
195
+ "language_model.model.layers.4.mlp.shared_experts.down_proj": "mxfp8",
196
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj": "mxfp8",
197
+ "language_model.model.layers.4.mlp.shared_experts.up_proj": "mxfp8",
198
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj": "mxfp8",
199
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj": "mxfp8",
200
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj": "mxfp8",
201
+ "language_model.model.layers.4.self_attn.k_proj": "mxfp8",
202
+ "language_model.model.layers.4.self_attn.o_proj": "mxfp8",
203
+ "language_model.model.layers.4.self_attn.q_proj": "mxfp8",
204
+ "language_model.model.layers.4.self_attn.v_proj": "mxfp8",
205
+ "language_model.model.layers.5.mlp.shared_experts.down_proj": "mxfp8",
206
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj": "mxfp8",
207
+ "language_model.model.layers.5.mlp.shared_experts.up_proj": "mxfp8",
208
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj": "mxfp8",
209
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj": "mxfp8",
210
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj": "mxfp8",
211
+ "language_model.model.layers.5.self_attn.k_proj": "mxfp8",
212
+ "language_model.model.layers.5.self_attn.o_proj": "mxfp8",
213
+ "language_model.model.layers.5.self_attn.q_proj": "mxfp8",
214
+ "language_model.model.layers.5.self_attn.v_proj": "mxfp8",
215
+ "language_model.model.layers.6.mlp.shared_experts.down_proj": "mxfp8",
216
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj": "mxfp8",
217
+ "language_model.model.layers.6.mlp.shared_experts.up_proj": "mxfp8",
218
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj": "mxfp8",
219
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj": "mxfp8",
220
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj": "mxfp8",
221
+ "language_model.model.layers.6.self_attn.k_proj": "mxfp8",
222
+ "language_model.model.layers.6.self_attn.o_proj": "mxfp8",
223
+ "language_model.model.layers.6.self_attn.q_proj": "mxfp8",
224
+ "language_model.model.layers.6.self_attn.v_proj": "mxfp8",
225
+ "language_model.model.layers.7.mlp.shared_experts.down_proj": "mxfp8",
226
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj": "mxfp8",
227
+ "language_model.model.layers.7.mlp.shared_experts.up_proj": "mxfp8",
228
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj": "mxfp8",
229
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj": "mxfp8",
230
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj": "mxfp8",
231
+ "language_model.model.layers.7.self_attn.k_proj": "mxfp8",
232
+ "language_model.model.layers.7.self_attn.o_proj": "mxfp8",
233
+ "language_model.model.layers.7.self_attn.q_proj": "mxfp8",
234
+ "language_model.model.layers.7.self_attn.v_proj": "mxfp8",
235
+ "language_model.model.layers.8.mlp.shared_experts.down_proj": "mxfp8",
236
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj": "mxfp8",
237
+ "language_model.model.layers.8.mlp.shared_experts.up_proj": "mxfp8",
238
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj": "mxfp8",
239
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj": "mxfp8",
240
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj": "mxfp8",
241
+ "language_model.model.layers.8.self_attn.k_proj": "mxfp8",
242
+ "language_model.model.layers.8.self_attn.o_proj": "mxfp8",
243
+ "language_model.model.layers.8.self_attn.q_proj": "mxfp8",
244
+ "language_model.model.layers.8.self_attn.v_proj": "mxfp8",
245
+ "language_model.model.layers.9.mlp.shared_experts.down_proj": "mxfp8",
246
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj": "mxfp8",
247
+ "language_model.model.layers.9.mlp.shared_experts.up_proj": "mxfp8",
248
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj": "mxfp8",
249
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj": "mxfp8",
250
+ "language_model.model.layers.9.mlp.switch_mlp.up_proj": "mxfp8",
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
+ }
release/bf16_accuracy.json ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "Unlimited-OCR",
3
+ "prompt": "<image>document parsing.",
4
+ "max_tokens": 1024,
5
+ "profile": "accurate",
6
+ "generation_settings": {
7
+ "temperature": 0.0,
8
+ "top_p": 1.0,
9
+ "repetition_penalty": 1.0,
10
+ "no_repeat_ngram_size": 35,
11
+ "ngram_window": 128
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
+ ]
195
+ }
release/calibration_results.json ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ {
39
+ "label": "mxfp8-head",
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
+ {
62
+ "label": "affine8-head",
63
+ "precision": "affine8",
64
+ "passed": true,
65
+ "checks": {
66
+ "cer": true,
67
+ "digit_cer": true,
68
+ "table_score": true,
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
+ ],
85
+ "selected": {
86
+ "label": "affine8-head",
87
+ "precision": "affine8"
88
+ },
89
+ "precision_overrides": {
90
+ "language_model.lm_head": "affine8"
91
+ }
92
+ }
release/candidate_accuracy.json ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
3
+ "prompt": "<image>document parsing.",
4
+ "max_tokens": 1024,
5
+ "profile": "accurate",
6
+ "generation_settings": {
7
+ "temperature": 0.0,
8
+ "top_p": 1.0,
9
+ "repetition_penalty": 1.0,
10
+ "no_repeat_ngram_size": 35,
11
+ "ngram_window": 128
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
+ ]
195
+ }
release/candidate_performance.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,
6
+ "system": {
7
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
8
+ "processor": "arm",
9
+ "python_version": "3.14.6",
10
+ "machine": "arm64",
11
+ "mlx_version": "0.32.0",
12
+ "mlx_vlm_version": "0.6.6",
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
+ ]
68
+ }
release/candidate_rswa.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
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",
26
+ "forced_minimum_tokens": 512,
27
+ "status": "success"
28
+ },
29
+ {
30
+ "max_tokens": 2048,
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,
43
+ "status": "success"
44
+ },
45
+ {
46
+ "max_tokens": 8192,
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,
59
+ "status": "success"
60
+ }
61
+ ],
62
+ "analysis": {
63
+ "cache_appears_bounded": true,
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,
71
+ "tps_stable": true,
72
+ "8k_test_passed": true
73
+ },
74
+ "passed": true
75
+ }
release/generated_precision_map.json ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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",
7
+ "language_model.model.layers.*.input_layernorm": "bfloat16",
8
+ "language_model.model.layers.*.post_attention_layernorm": "bfloat16",
9
+ "language_model.model.layers.*.self_attn.q_proj": "mxfp8",
10
+ "language_model.model.layers.*.self_attn.k_proj": "mxfp8",
11
+ "language_model.model.layers.*.self_attn.v_proj": "mxfp8",
12
+ "language_model.model.layers.*.self_attn.o_proj": "mxfp8",
13
+ "language_model.model.layers.*.mlp.gate_proj": "mxfp8",
14
+ "language_model.model.layers.*.mlp.up_proj": "mxfp8",
15
+ "language_model.model.layers.*.mlp.down_proj": "mxfp8",
16
+ "language_model.model.layers.*.mlp.shared_experts.gate_proj": "mxfp8",
17
+ "language_model.model.layers.*.mlp.shared_experts.up_proj": "mxfp8",
18
+ "language_model.model.layers.*.mlp.shared_experts.down_proj": "mxfp8",
19
+ "language_model.model.layers.*.mlp.switch_mlp.gate_proj": "mxfp8",
20
+ "language_model.model.layers.*.mlp.switch_mlp.up_proj": "mxfp8",
21
+ "language_model.model.layers.*.mlp.switch_mlp.down_proj": "mxfp8",
22
+ "language_model.model.layers.*.mlp.gate": "bfloat16",
23
+ "sam_model": "bfloat16",
24
+ "_generated_from": {
25
+ "source_model": "baidu/Unlimited-OCR",
26
+ "thresholds": {
27
+ "cer_delta": 0.02,
28
+ "digit_cer_delta": 0.02,
29
+ "table_score_degradation": 0.01
30
+ },
31
+ "decisions": {
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": {
48
+ "precision": "mxfp8",
49
+ "reasons": [
50
+ "all measured quality deltas were non-degrading"
51
+ ]
52
+ },
53
+ "attention_q_proj": {
54
+ "precision": "base-map",
55
+ "reasons": [
56
+ "within thresholds"
57
+ ]
58
+ },
59
+ "attention_k_proj": {
60
+ "precision": "base-map",
61
+ "reasons": [
62
+ "within thresholds"
63
+ ]
64
+ },
65
+ "attention_v_proj": {
66
+ "precision": "base-map",
67
+ "reasons": [
68
+ "within thresholds"
69
+ ]
70
+ },
71
+ "attention_o_proj": {
72
+ "precision": "base-map",
73
+ "reasons": [
74
+ "within thresholds"
75
+ ]
76
+ },
77
+ "dense_mlp": {
78
+ "precision": "base-map",
79
+ "reasons": [
80
+ "within thresholds"
81
+ ]
82
+ },
83
+ "shared_experts": {
84
+ "precision": "base-map",
85
+ "reasons": [
86
+ "within thresholds"
87
+ ]
88
+ },
89
+ "routed_experts": {
90
+ "precision": "base-map",
91
+ "reasons": [
92
+ "within thresholds"
93
+ ]
94
+ },
95
+ "lm_head": {
96
+ "precision": "affine8",
97
+ "reasons": [
98
+ "selected by joint quality/throughput calibration: affine8-head"
99
+ ]
100
+ }
101
+ },
102
+ "calibration": {
103
+ "selected": {
104
+ "label": "affine8-head",
105
+ "precision": "affine8"
106
+ },
107
+ "selection_policy": "fastest candidate passing existing quality and throughput limits"
108
+ }
109
+ }
110
+ }
release/provenance.json ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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",
6
+ "processor": "arm",
7
+ "versions": {
8
+ "mlx": "0.32.0",
9
+ "mlx-vlm": "0.6.6",
10
+ "huggingface-hub": "1.24.0",
11
+ "numpy": "2.5.1",
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",
39
+ "size": 243645,
40
+ "sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
41
+ },
42
+ {
43
+ "path": "ground_truth/0001.txt",
44
+ "size": 1346,
45
+ "sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
46
+ },
47
+ {
48
+ "path": "images/0002.png",
49
+ "size": 208699,
50
+ "sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
51
+ },
52
+ {
53
+ "path": "ground_truth/0002.txt",
54
+ "size": 838,
55
+ "sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
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
+ },
159
+ "smoke_image": {
160
+ "name": "test_invoice.png",
161
+ "sha256": "6f2c3a686d13604b6721b1e9596408ec3b3eb38227b6d5f2d930a0ce05c8cb14"
162
+ },
163
+ "parameters": {
164
+ "accuracy_tokens": 1024,
165
+ "performance_tokens": 256,
166
+ "performance_warmup": 1,
167
+ "performance_runs": 3,
168
+ "rswa_lengths": [
169
+ 512,
170
+ 2048,
171
+ 8192
172
+ ]
173
+ }
174
+ }
release/reference_accuracy.json ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
3
+ "prompt": "<image>document parsing.",
4
+ "max_tokens": 1024,
5
+ "profile": "accurate",
6
+ "generation_settings": {
7
+ "temperature": 0.0,
8
+ "top_p": 1.0,
9
+ "repetition_penalty": 1.0,
10
+ "no_repeat_ngram_size": 35,
11
+ "ngram_window": 128
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
+ ]
195
+ }
release/reference_performance.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
3
+ "image_path": "test_invoice.png",
4
+ "prompt": "<image>document parsing.",
5
+ "max_tokens": 256,
6
+ "system": {
7
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
8
+ "processor": "arm",
9
+ "python_version": "3.14.6",
10
+ "machine": "arm64",
11
+ "mlx_version": "0.32.0",
12
+ "mlx_vlm_version": "0.6.6",
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
+ ]
68
+ }
release/release_manifest.json ADDED
@@ -0,0 +1,1191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ {
11
+ "name": "model-00001-of-000001.safetensors",
12
+ "size": 6672547120,
13
+ "sha256": "2bc48a7a110061ea58fff65d3169367eebe3aee371ca6968dc2219c1b2855fc6"
14
+ }
15
+ ],
16
+ "total_size_bytes": 6672547120,
17
+ "total_size_gb": 6.214293763041496,
18
+ "aggregate_sha256": "ede2e0c0a6261543dd0e2eb249c337a3409b2b69b8f9ed73fa604ac409bdf8f2"
19
+ }
20
+ },
21
+ "reference": {
22
+ "id": "sahilchachra/unlimited-ocr-mxfp8-mlx",
23
+ "weights": {
24
+ "files": [
25
+ {
26
+ "name": "model.safetensors",
27
+ "size": 3827646853,
28
+ "sha256": "eef3e920d4ef9429ef6353b4b98c34ed8935f678f91ff3bcf7de8ed73a7530db"
29
+ }
30
+ ],
31
+ "total_size_bytes": 3827646853,
32
+ "total_size_gb": 3.564773921854794,
33
+ "aggregate_sha256": "439c6acfe5e277537dfe3368b94a145a6b0da4c39e4bd00582977d030f292ad1"
34
+ }
35
+ },
36
+ "candidate": {
37
+ "name": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
38
+ "weights": {
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,
52
+ "checks": {
53
+ "architecture": true,
54
+ "model_type": true,
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",
80
+ "size": 243645,
81
+ "sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
82
+ },
83
+ {
84
+ "path": "ground_truth/0001.txt",
85
+ "size": 1346,
86
+ "sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
87
+ },
88
+ {
89
+ "path": "images/0002.png",
90
+ "size": 208699,
91
+ "sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
92
+ },
93
+ {
94
+ "path": "ground_truth/0002.txt",
95
+ "size": 838,
96
+ "sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
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,
217
+ "profile": "accurate"
218
+ },
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,
227
+ "profile": "accurate"
228
+ },
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
+ },
252
+ "rswa": {
253
+ "model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
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",
277
+ "forced_minimum_tokens": 512,
278
+ "status": "success"
279
+ },
280
+ {
281
+ "max_tokens": 2048,
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,
294
+ "status": "success"
295
+ },
296
+ {
297
+ "max_tokens": 8192,
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,
310
+ "status": "success"
311
+ }
312
+ ],
313
+ "analysis": {
314
+ "cache_appears_bounded": true,
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,
322
+ "tps_stable": true,
323
+ "8k_test_passed": true
324
+ },
325
+ "passed": true
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": {
340
+ "status": "success",
341
+ "matched_module_count": 145,
342
+ "matched_modules": [
343
+ "sam_model.blocks.0.attn.proj",
344
+ "sam_model.blocks.0.attn.qkv",
345
+ "sam_model.blocks.0.mlp.lin1",
346
+ "sam_model.blocks.0.mlp.lin2",
347
+ "sam_model.blocks.1.attn.proj",
348
+ "sam_model.blocks.1.attn.qkv",
349
+ "sam_model.blocks.1.mlp.lin1",
350
+ "sam_model.blocks.1.mlp.lin2",
351
+ "sam_model.blocks.10.attn.proj",
352
+ "sam_model.blocks.10.attn.qkv",
353
+ "sam_model.blocks.10.mlp.lin1",
354
+ "sam_model.blocks.10.mlp.lin2",
355
+ "sam_model.blocks.11.attn.proj",
356
+ "sam_model.blocks.11.attn.qkv",
357
+ "sam_model.blocks.11.mlp.lin1",
358
+ "sam_model.blocks.11.mlp.lin2",
359
+ "sam_model.blocks.2.attn.proj",
360
+ "sam_model.blocks.2.attn.qkv",
361
+ "sam_model.blocks.2.mlp.lin1",
362
+ "sam_model.blocks.2.mlp.lin2",
363
+ "sam_model.blocks.3.attn.proj",
364
+ "sam_model.blocks.3.attn.qkv",
365
+ "sam_model.blocks.3.mlp.lin1",
366
+ "sam_model.blocks.3.mlp.lin2",
367
+ "sam_model.blocks.4.attn.proj",
368
+ "sam_model.blocks.4.attn.qkv",
369
+ "sam_model.blocks.4.mlp.lin1",
370
+ "sam_model.blocks.4.mlp.lin2",
371
+ "sam_model.blocks.5.attn.proj",
372
+ "sam_model.blocks.5.attn.qkv",
373
+ "sam_model.blocks.5.mlp.lin1",
374
+ "sam_model.blocks.5.mlp.lin2",
375
+ "sam_model.blocks.6.attn.proj",
376
+ "sam_model.blocks.6.attn.qkv",
377
+ "sam_model.blocks.6.mlp.lin1",
378
+ "sam_model.blocks.6.mlp.lin2",
379
+ "sam_model.blocks.7.attn.proj",
380
+ "sam_model.blocks.7.attn.qkv",
381
+ "sam_model.blocks.7.mlp.lin1",
382
+ "sam_model.blocks.7.mlp.lin2",
383
+ "sam_model.blocks.8.attn.proj",
384
+ "sam_model.blocks.8.attn.qkv",
385
+ "sam_model.blocks.8.mlp.lin1",
386
+ "sam_model.blocks.8.mlp.lin2",
387
+ "sam_model.blocks.9.attn.proj",
388
+ "sam_model.blocks.9.attn.qkv",
389
+ "sam_model.blocks.9.mlp.lin1",
390
+ "sam_model.blocks.9.mlp.lin2",
391
+ "vision_model.embeddings.position_embedding",
392
+ "vision_model.transformer.layers.0.mlp.fc1",
393
+ "vision_model.transformer.layers.0.mlp.fc2",
394
+ "vision_model.transformer.layers.0.self_attn.out_proj",
395
+ "vision_model.transformer.layers.0.self_attn.qkv_proj",
396
+ "vision_model.transformer.layers.1.mlp.fc1",
397
+ "vision_model.transformer.layers.1.mlp.fc2",
398
+ "vision_model.transformer.layers.1.self_attn.out_proj",
399
+ "vision_model.transformer.layers.1.self_attn.qkv_proj",
400
+ "vision_model.transformer.layers.10.mlp.fc1",
401
+ "vision_model.transformer.layers.10.mlp.fc2",
402
+ "vision_model.transformer.layers.10.self_attn.out_proj",
403
+ "vision_model.transformer.layers.10.self_attn.qkv_proj",
404
+ "vision_model.transformer.layers.11.mlp.fc1",
405
+ "vision_model.transformer.layers.11.mlp.fc2",
406
+ "vision_model.transformer.layers.11.self_attn.out_proj",
407
+ "vision_model.transformer.layers.11.self_attn.qkv_proj",
408
+ "vision_model.transformer.layers.12.mlp.fc1",
409
+ "vision_model.transformer.layers.12.mlp.fc2",
410
+ "vision_model.transformer.layers.12.self_attn.out_proj",
411
+ "vision_model.transformer.layers.12.self_attn.qkv_proj",
412
+ "vision_model.transformer.layers.13.mlp.fc1",
413
+ "vision_model.transformer.layers.13.mlp.fc2",
414
+ "vision_model.transformer.layers.13.self_attn.out_proj",
415
+ "vision_model.transformer.layers.13.self_attn.qkv_proj",
416
+ "vision_model.transformer.layers.14.mlp.fc1",
417
+ "vision_model.transformer.layers.14.mlp.fc2",
418
+ "vision_model.transformer.layers.14.self_attn.out_proj",
419
+ "vision_model.transformer.layers.14.self_attn.qkv_proj",
420
+ "vision_model.transformer.layers.15.mlp.fc1",
421
+ "vision_model.transformer.layers.15.mlp.fc2",
422
+ "vision_model.transformer.layers.15.self_attn.out_proj",
423
+ "vision_model.transformer.layers.15.self_attn.qkv_proj",
424
+ "vision_model.transformer.layers.16.mlp.fc1",
425
+ "vision_model.transformer.layers.16.mlp.fc2",
426
+ "vision_model.transformer.layers.16.self_attn.out_proj",
427
+ "vision_model.transformer.layers.16.self_attn.qkv_proj",
428
+ "vision_model.transformer.layers.17.mlp.fc1",
429
+ "vision_model.transformer.layers.17.mlp.fc2",
430
+ "vision_model.transformer.layers.17.self_attn.out_proj",
431
+ "vision_model.transformer.layers.17.self_attn.qkv_proj",
432
+ "vision_model.transformer.layers.18.mlp.fc1",
433
+ "vision_model.transformer.layers.18.mlp.fc2",
434
+ "vision_model.transformer.layers.18.self_attn.out_proj",
435
+ "vision_model.transformer.layers.18.self_attn.qkv_proj",
436
+ "vision_model.transformer.layers.19.mlp.fc1",
437
+ "vision_model.transformer.layers.19.mlp.fc2",
438
+ "vision_model.transformer.layers.19.self_attn.out_proj",
439
+ "vision_model.transformer.layers.19.self_attn.qkv_proj",
440
+ "vision_model.transformer.layers.2.mlp.fc1",
441
+ "vision_model.transformer.layers.2.mlp.fc2",
442
+ "vision_model.transformer.layers.2.self_attn.out_proj",
443
+ "vision_model.transformer.layers.2.self_attn.qkv_proj",
444
+ "vision_model.transformer.layers.20.mlp.fc1",
445
+ "vision_model.transformer.layers.20.mlp.fc2",
446
+ "vision_model.transformer.layers.20.self_attn.out_proj",
447
+ "vision_model.transformer.layers.20.self_attn.qkv_proj",
448
+ "vision_model.transformer.layers.21.mlp.fc1",
449
+ "vision_model.transformer.layers.21.mlp.fc2",
450
+ "vision_model.transformer.layers.21.self_attn.out_proj",
451
+ "vision_model.transformer.layers.21.self_attn.qkv_proj",
452
+ "vision_model.transformer.layers.22.mlp.fc1",
453
+ "vision_model.transformer.layers.22.mlp.fc2",
454
+ "vision_model.transformer.layers.22.self_attn.out_proj",
455
+ "vision_model.transformer.layers.22.self_attn.qkv_proj",
456
+ "vision_model.transformer.layers.23.mlp.fc1",
457
+ "vision_model.transformer.layers.23.mlp.fc2",
458
+ "vision_model.transformer.layers.23.self_attn.out_proj",
459
+ "vision_model.transformer.layers.23.self_attn.qkv_proj",
460
+ "vision_model.transformer.layers.3.mlp.fc1",
461
+ "vision_model.transformer.layers.3.mlp.fc2",
462
+ "vision_model.transformer.layers.3.self_attn.out_proj",
463
+ "vision_model.transformer.layers.3.self_attn.qkv_proj",
464
+ "vision_model.transformer.layers.4.mlp.fc1",
465
+ "vision_model.transformer.layers.4.mlp.fc2",
466
+ "vision_model.transformer.layers.4.self_attn.out_proj",
467
+ "vision_model.transformer.layers.4.self_attn.qkv_proj",
468
+ "vision_model.transformer.layers.5.mlp.fc1",
469
+ "vision_model.transformer.layers.5.mlp.fc2",
470
+ "vision_model.transformer.layers.5.self_attn.out_proj",
471
+ "vision_model.transformer.layers.5.self_attn.qkv_proj",
472
+ "vision_model.transformer.layers.6.mlp.fc1",
473
+ "vision_model.transformer.layers.6.mlp.fc2",
474
+ "vision_model.transformer.layers.6.self_attn.out_proj",
475
+ "vision_model.transformer.layers.6.self_attn.qkv_proj",
476
+ "vision_model.transformer.layers.7.mlp.fc1",
477
+ "vision_model.transformer.layers.7.mlp.fc2",
478
+ "vision_model.transformer.layers.7.self_attn.out_proj",
479
+ "vision_model.transformer.layers.7.self_attn.qkv_proj",
480
+ "vision_model.transformer.layers.8.mlp.fc1",
481
+ "vision_model.transformer.layers.8.mlp.fc2",
482
+ "vision_model.transformer.layers.8.self_attn.out_proj",
483
+ "vision_model.transformer.layers.8.self_attn.qkv_proj",
484
+ "vision_model.transformer.layers.9.mlp.fc1",
485
+ "vision_model.transformer.layers.9.mlp.fc2",
486
+ "vision_model.transformer.layers.9.self_attn.out_proj",
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
+ },
504
+ "vision_projector": {
505
+ "status": "success",
506
+ "matched_module_count": 1,
507
+ "matched_modules": [
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",
527
+ "matched_module_count": 1,
528
+ "matched_modules": [
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
+ },
546
+ "attention_q_proj": {
547
+ "status": "success",
548
+ "matched_module_count": 12,
549
+ "matched_modules": [
550
+ "language_model.model.layers.0.self_attn.q_proj",
551
+ "language_model.model.layers.1.self_attn.q_proj",
552
+ "language_model.model.layers.10.self_attn.q_proj",
553
+ "language_model.model.layers.11.self_attn.q_proj",
554
+ "language_model.model.layers.2.self_attn.q_proj",
555
+ "language_model.model.layers.3.self_attn.q_proj",
556
+ "language_model.model.layers.4.self_attn.q_proj",
557
+ "language_model.model.layers.5.self_attn.q_proj",
558
+ "language_model.model.layers.6.self_attn.q_proj",
559
+ "language_model.model.layers.7.self_attn.q_proj",
560
+ "language_model.model.layers.8.self_attn.q_proj",
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
+ },
578
+ "attention_k_proj": {
579
+ "status": "success",
580
+ "matched_module_count": 12,
581
+ "matched_modules": [
582
+ "language_model.model.layers.0.self_attn.k_proj",
583
+ "language_model.model.layers.1.self_attn.k_proj",
584
+ "language_model.model.layers.10.self_attn.k_proj",
585
+ "language_model.model.layers.11.self_attn.k_proj",
586
+ "language_model.model.layers.2.self_attn.k_proj",
587
+ "language_model.model.layers.3.self_attn.k_proj",
588
+ "language_model.model.layers.4.self_attn.k_proj",
589
+ "language_model.model.layers.5.self_attn.k_proj",
590
+ "language_model.model.layers.6.self_attn.k_proj",
591
+ "language_model.model.layers.7.self_attn.k_proj",
592
+ "language_model.model.layers.8.self_attn.k_proj",
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
+ },
610
+ "attention_v_proj": {
611
+ "status": "success",
612
+ "matched_module_count": 12,
613
+ "matched_modules": [
614
+ "language_model.model.layers.0.self_attn.v_proj",
615
+ "language_model.model.layers.1.self_attn.v_proj",
616
+ "language_model.model.layers.10.self_attn.v_proj",
617
+ "language_model.model.layers.11.self_attn.v_proj",
618
+ "language_model.model.layers.2.self_attn.v_proj",
619
+ "language_model.model.layers.3.self_attn.v_proj",
620
+ "language_model.model.layers.4.self_attn.v_proj",
621
+ "language_model.model.layers.5.self_attn.v_proj",
622
+ "language_model.model.layers.6.self_attn.v_proj",
623
+ "language_model.model.layers.7.self_attn.v_proj",
624
+ "language_model.model.layers.8.self_attn.v_proj",
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",
644
+ "matched_module_count": 12,
645
+ "matched_modules": [
646
+ "language_model.model.layers.0.self_attn.o_proj",
647
+ "language_model.model.layers.1.self_attn.o_proj",
648
+ "language_model.model.layers.10.self_attn.o_proj",
649
+ "language_model.model.layers.11.self_attn.o_proj",
650
+ "language_model.model.layers.2.self_attn.o_proj",
651
+ "language_model.model.layers.3.self_attn.o_proj",
652
+ "language_model.model.layers.4.self_attn.o_proj",
653
+ "language_model.model.layers.5.self_attn.o_proj",
654
+ "language_model.model.layers.6.self_attn.o_proj",
655
+ "language_model.model.layers.7.self_attn.o_proj",
656
+ "language_model.model.layers.8.self_attn.o_proj",
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",
676
+ "matched_module_count": 3,
677
+ "matched_modules": [
678
+ "language_model.model.layers.0.mlp.down_proj",
679
+ "language_model.model.layers.0.mlp.gate_proj",
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",
699
+ "matched_module_count": 33,
700
+ "matched_modules": [
701
+ "language_model.model.layers.1.mlp.shared_experts.down_proj",
702
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj",
703
+ "language_model.model.layers.1.mlp.shared_experts.up_proj",
704
+ "language_model.model.layers.10.mlp.shared_experts.down_proj",
705
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj",
706
+ "language_model.model.layers.10.mlp.shared_experts.up_proj",
707
+ "language_model.model.layers.11.mlp.shared_experts.down_proj",
708
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj",
709
+ "language_model.model.layers.11.mlp.shared_experts.up_proj",
710
+ "language_model.model.layers.2.mlp.shared_experts.down_proj",
711
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj",
712
+ "language_model.model.layers.2.mlp.shared_experts.up_proj",
713
+ "language_model.model.layers.3.mlp.shared_experts.down_proj",
714
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj",
715
+ "language_model.model.layers.3.mlp.shared_experts.up_proj",
716
+ "language_model.model.layers.4.mlp.shared_experts.down_proj",
717
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj",
718
+ "language_model.model.layers.4.mlp.shared_experts.up_proj",
719
+ "language_model.model.layers.5.mlp.shared_experts.down_proj",
720
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj",
721
+ "language_model.model.layers.5.mlp.shared_experts.up_proj",
722
+ "language_model.model.layers.6.mlp.shared_experts.down_proj",
723
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj",
724
+ "language_model.model.layers.6.mlp.shared_experts.up_proj",
725
+ "language_model.model.layers.7.mlp.shared_experts.down_proj",
726
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj",
727
+ "language_model.model.layers.7.mlp.shared_experts.up_proj",
728
+ "language_model.model.layers.8.mlp.shared_experts.down_proj",
729
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj",
730
+ "language_model.model.layers.8.mlp.shared_experts.up_proj",
731
+ "language_model.model.layers.9.mlp.shared_experts.down_proj",
732
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj",
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",
752
+ "matched_module_count": 33,
753
+ "matched_modules": [
754
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj",
755
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj",
756
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj",
757
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj",
758
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj",
759
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj",
760
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj",
761
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj",
762
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj",
763
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj",
764
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj",
765
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj",
766
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj",
767
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj",
768
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj",
769
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj",
770
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj",
771
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj",
772
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj",
773
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj",
774
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj",
775
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj",
776
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj",
777
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj",
778
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj",
779
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj",
780
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj",
781
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj",
782
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj",
783
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj",
784
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj",
785
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj",
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",
805
+ "matched_module_count": 1,
806
+ "matched_modules": [
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
+ {
864
+ "label": "mxfp8-head",
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
+ {
887
+ "label": "affine8-head",
888
+ "precision": "affine8",
889
+ "passed": true,
890
+ "checks": {
891
+ "cer": true,
892
+ "digit_cer": true,
893
+ "table_score": true,
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
+ ],
910
+ "selected": {
911
+ "label": "affine8-head",
912
+ "precision": "affine8"
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",
924
+ "language_model.model.layers.*.input_layernorm": "bfloat16",
925
+ "language_model.model.layers.*.post_attention_layernorm": "bfloat16",
926
+ "language_model.model.layers.*.self_attn.q_proj": "mxfp8",
927
+ "language_model.model.layers.*.self_attn.k_proj": "mxfp8",
928
+ "language_model.model.layers.*.self_attn.v_proj": "mxfp8",
929
+ "language_model.model.layers.*.self_attn.o_proj": "mxfp8",
930
+ "language_model.model.layers.*.mlp.gate_proj": "mxfp8",
931
+ "language_model.model.layers.*.mlp.up_proj": "mxfp8",
932
+ "language_model.model.layers.*.mlp.down_proj": "mxfp8",
933
+ "language_model.model.layers.*.mlp.shared_experts.gate_proj": "mxfp8",
934
+ "language_model.model.layers.*.mlp.shared_experts.up_proj": "mxfp8",
935
+ "language_model.model.layers.*.mlp.shared_experts.down_proj": "mxfp8",
936
+ "language_model.model.layers.*.mlp.switch_mlp.gate_proj": "mxfp8",
937
+ "language_model.model.layers.*.mlp.switch_mlp.up_proj": "mxfp8",
938
+ "language_model.model.layers.*.mlp.switch_mlp.down_proj": "mxfp8",
939
+ "language_model.model.layers.*.mlp.gate": "bfloat16",
940
+ "sam_model": "bfloat16",
941
+ "_generated_from": {
942
+ "source_model": "baidu/Unlimited-OCR",
943
+ "thresholds": {
944
+ "cer_delta": 0.02,
945
+ "digit_cer_delta": 0.02,
946
+ "table_score_degradation": 0.01
947
+ },
948
+ "decisions": {
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": {
965
+ "precision": "mxfp8",
966
+ "reasons": [
967
+ "all measured quality deltas were non-degrading"
968
+ ]
969
+ },
970
+ "attention_q_proj": {
971
+ "precision": "base-map",
972
+ "reasons": [
973
+ "within thresholds"
974
+ ]
975
+ },
976
+ "attention_k_proj": {
977
+ "precision": "base-map",
978
+ "reasons": [
979
+ "within thresholds"
980
+ ]
981
+ },
982
+ "attention_v_proj": {
983
+ "precision": "base-map",
984
+ "reasons": [
985
+ "within thresholds"
986
+ ]
987
+ },
988
+ "attention_o_proj": {
989
+ "precision": "base-map",
990
+ "reasons": [
991
+ "within thresholds"
992
+ ]
993
+ },
994
+ "dense_mlp": {
995
+ "precision": "base-map",
996
+ "reasons": [
997
+ "within thresholds"
998
+ ]
999
+ },
1000
+ "shared_experts": {
1001
+ "precision": "base-map",
1002
+ "reasons": [
1003
+ "within thresholds"
1004
+ ]
1005
+ },
1006
+ "routed_experts": {
1007
+ "precision": "base-map",
1008
+ "reasons": [
1009
+ "within thresholds"
1010
+ ]
1011
+ },
1012
+ "lm_head": {
1013
+ "precision": "affine8",
1014
+ "reasons": [
1015
+ "selected by joint quality/throughput calibration: affine8-head"
1016
+ ]
1017
+ }
1018
+ },
1019
+ "calibration": {
1020
+ "selected": {
1021
+ "label": "affine8-head",
1022
+ "precision": "affine8"
1023
+ },
1024
+ "selection_policy": "fastest candidate passing existing quality and throughput limits"
1025
+ }
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
+ },
1095
+ {
1096
+ "name": "native_model_metadata",
1097
+ "passed": true,
1098
+ "actual": {
1099
+ "architecture": true,
1100
+ "model_type": true,
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,
1112
+ "actual": {
1113
+ "samples": 12,
1114
+ "digit": 12,
1115
+ "cjk": 3,
1116
+ "table": 3
1117
+ },
1118
+ "limit": "all counts > 0",
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
+ },
1153
+ {
1154
+ "name": "candidate_table_score_vs_bf16",
1155
+ "passed": true,
1156
+ "actual": 0.0,
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
+ },
1167
+ {
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": {
1180
+ "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
1181
+ "machine": "arm64",
1182
+ "python": "3.14.6",
1183
+ "versions": {
1184
+ "mlx": "0.32.0",
1185
+ "mlx-vlm": "0.6.6",
1186
+ "huggingface-hub": "1.24.0",
1187
+ "numpy": "2.5.1",
1188
+ "Pillow": "12.3.0"
1189
+ }
1190
+ }
1191
+ }
release/sensitivity_results.json ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": [
17
+ "vision_encoder",
18
+ "vision_projector",
19
+ "token_embeddings",
20
+ "attention_q_proj",
21
+ "attention_k_proj",
22
+ "attention_v_proj",
23
+ "attention_o_proj",
24
+ "dense_mlp",
25
+ "shared_experts",
26
+ "routed_experts",
27
+ "lm_head"
28
+ ],
29
+ "layer_groups": {
30
+ "vision_encoder": {
31
+ "status": "success",
32
+ "matched_module_count": 145,
33
+ "matched_modules": [
34
+ "sam_model.blocks.0.attn.proj",
35
+ "sam_model.blocks.0.attn.qkv",
36
+ "sam_model.blocks.0.mlp.lin1",
37
+ "sam_model.blocks.0.mlp.lin2",
38
+ "sam_model.blocks.1.attn.proj",
39
+ "sam_model.blocks.1.attn.qkv",
40
+ "sam_model.blocks.1.mlp.lin1",
41
+ "sam_model.blocks.1.mlp.lin2",
42
+ "sam_model.blocks.10.attn.proj",
43
+ "sam_model.blocks.10.attn.qkv",
44
+ "sam_model.blocks.10.mlp.lin1",
45
+ "sam_model.blocks.10.mlp.lin2",
46
+ "sam_model.blocks.11.attn.proj",
47
+ "sam_model.blocks.11.attn.qkv",
48
+ "sam_model.blocks.11.mlp.lin1",
49
+ "sam_model.blocks.11.mlp.lin2",
50
+ "sam_model.blocks.2.attn.proj",
51
+ "sam_model.blocks.2.attn.qkv",
52
+ "sam_model.blocks.2.mlp.lin1",
53
+ "sam_model.blocks.2.mlp.lin2",
54
+ "sam_model.blocks.3.attn.proj",
55
+ "sam_model.blocks.3.attn.qkv",
56
+ "sam_model.blocks.3.mlp.lin1",
57
+ "sam_model.blocks.3.mlp.lin2",
58
+ "sam_model.blocks.4.attn.proj",
59
+ "sam_model.blocks.4.attn.qkv",
60
+ "sam_model.blocks.4.mlp.lin1",
61
+ "sam_model.blocks.4.mlp.lin2",
62
+ "sam_model.blocks.5.attn.proj",
63
+ "sam_model.blocks.5.attn.qkv",
64
+ "sam_model.blocks.5.mlp.lin1",
65
+ "sam_model.blocks.5.mlp.lin2",
66
+ "sam_model.blocks.6.attn.proj",
67
+ "sam_model.blocks.6.attn.qkv",
68
+ "sam_model.blocks.6.mlp.lin1",
69
+ "sam_model.blocks.6.mlp.lin2",
70
+ "sam_model.blocks.7.attn.proj",
71
+ "sam_model.blocks.7.attn.qkv",
72
+ "sam_model.blocks.7.mlp.lin1",
73
+ "sam_model.blocks.7.mlp.lin2",
74
+ "sam_model.blocks.8.attn.proj",
75
+ "sam_model.blocks.8.attn.qkv",
76
+ "sam_model.blocks.8.mlp.lin1",
77
+ "sam_model.blocks.8.mlp.lin2",
78
+ "sam_model.blocks.9.attn.proj",
79
+ "sam_model.blocks.9.attn.qkv",
80
+ "sam_model.blocks.9.mlp.lin1",
81
+ "sam_model.blocks.9.mlp.lin2",
82
+ "vision_model.embeddings.position_embedding",
83
+ "vision_model.transformer.layers.0.mlp.fc1",
84
+ "vision_model.transformer.layers.0.mlp.fc2",
85
+ "vision_model.transformer.layers.0.self_attn.out_proj",
86
+ "vision_model.transformer.layers.0.self_attn.qkv_proj",
87
+ "vision_model.transformer.layers.1.mlp.fc1",
88
+ "vision_model.transformer.layers.1.mlp.fc2",
89
+ "vision_model.transformer.layers.1.self_attn.out_proj",
90
+ "vision_model.transformer.layers.1.self_attn.qkv_proj",
91
+ "vision_model.transformer.layers.10.mlp.fc1",
92
+ "vision_model.transformer.layers.10.mlp.fc2",
93
+ "vision_model.transformer.layers.10.self_attn.out_proj",
94
+ "vision_model.transformer.layers.10.self_attn.qkv_proj",
95
+ "vision_model.transformer.layers.11.mlp.fc1",
96
+ "vision_model.transformer.layers.11.mlp.fc2",
97
+ "vision_model.transformer.layers.11.self_attn.out_proj",
98
+ "vision_model.transformer.layers.11.self_attn.qkv_proj",
99
+ "vision_model.transformer.layers.12.mlp.fc1",
100
+ "vision_model.transformer.layers.12.mlp.fc2",
101
+ "vision_model.transformer.layers.12.self_attn.out_proj",
102
+ "vision_model.transformer.layers.12.self_attn.qkv_proj",
103
+ "vision_model.transformer.layers.13.mlp.fc1",
104
+ "vision_model.transformer.layers.13.mlp.fc2",
105
+ "vision_model.transformer.layers.13.self_attn.out_proj",
106
+ "vision_model.transformer.layers.13.self_attn.qkv_proj",
107
+ "vision_model.transformer.layers.14.mlp.fc1",
108
+ "vision_model.transformer.layers.14.mlp.fc2",
109
+ "vision_model.transformer.layers.14.self_attn.out_proj",
110
+ "vision_model.transformer.layers.14.self_attn.qkv_proj",
111
+ "vision_model.transformer.layers.15.mlp.fc1",
112
+ "vision_model.transformer.layers.15.mlp.fc2",
113
+ "vision_model.transformer.layers.15.self_attn.out_proj",
114
+ "vision_model.transformer.layers.15.self_attn.qkv_proj",
115
+ "vision_model.transformer.layers.16.mlp.fc1",
116
+ "vision_model.transformer.layers.16.mlp.fc2",
117
+ "vision_model.transformer.layers.16.self_attn.out_proj",
118
+ "vision_model.transformer.layers.16.self_attn.qkv_proj",
119
+ "vision_model.transformer.layers.17.mlp.fc1",
120
+ "vision_model.transformer.layers.17.mlp.fc2",
121
+ "vision_model.transformer.layers.17.self_attn.out_proj",
122
+ "vision_model.transformer.layers.17.self_attn.qkv_proj",
123
+ "vision_model.transformer.layers.18.mlp.fc1",
124
+ "vision_model.transformer.layers.18.mlp.fc2",
125
+ "vision_model.transformer.layers.18.self_attn.out_proj",
126
+ "vision_model.transformer.layers.18.self_attn.qkv_proj",
127
+ "vision_model.transformer.layers.19.mlp.fc1",
128
+ "vision_model.transformer.layers.19.mlp.fc2",
129
+ "vision_model.transformer.layers.19.self_attn.out_proj",
130
+ "vision_model.transformer.layers.19.self_attn.qkv_proj",
131
+ "vision_model.transformer.layers.2.mlp.fc1",
132
+ "vision_model.transformer.layers.2.mlp.fc2",
133
+ "vision_model.transformer.layers.2.self_attn.out_proj",
134
+ "vision_model.transformer.layers.2.self_attn.qkv_proj",
135
+ "vision_model.transformer.layers.20.mlp.fc1",
136
+ "vision_model.transformer.layers.20.mlp.fc2",
137
+ "vision_model.transformer.layers.20.self_attn.out_proj",
138
+ "vision_model.transformer.layers.20.self_attn.qkv_proj",
139
+ "vision_model.transformer.layers.21.mlp.fc1",
140
+ "vision_model.transformer.layers.21.mlp.fc2",
141
+ "vision_model.transformer.layers.21.self_attn.out_proj",
142
+ "vision_model.transformer.layers.21.self_attn.qkv_proj",
143
+ "vision_model.transformer.layers.22.mlp.fc1",
144
+ "vision_model.transformer.layers.22.mlp.fc2",
145
+ "vision_model.transformer.layers.22.self_attn.out_proj",
146
+ "vision_model.transformer.layers.22.self_attn.qkv_proj",
147
+ "vision_model.transformer.layers.23.mlp.fc1",
148
+ "vision_model.transformer.layers.23.mlp.fc2",
149
+ "vision_model.transformer.layers.23.self_attn.out_proj",
150
+ "vision_model.transformer.layers.23.self_attn.qkv_proj",
151
+ "vision_model.transformer.layers.3.mlp.fc1",
152
+ "vision_model.transformer.layers.3.mlp.fc2",
153
+ "vision_model.transformer.layers.3.self_attn.out_proj",
154
+ "vision_model.transformer.layers.3.self_attn.qkv_proj",
155
+ "vision_model.transformer.layers.4.mlp.fc1",
156
+ "vision_model.transformer.layers.4.mlp.fc2",
157
+ "vision_model.transformer.layers.4.self_attn.out_proj",
158
+ "vision_model.transformer.layers.4.self_attn.qkv_proj",
159
+ "vision_model.transformer.layers.5.mlp.fc1",
160
+ "vision_model.transformer.layers.5.mlp.fc2",
161
+ "vision_model.transformer.layers.5.self_attn.out_proj",
162
+ "vision_model.transformer.layers.5.self_attn.qkv_proj",
163
+ "vision_model.transformer.layers.6.mlp.fc1",
164
+ "vision_model.transformer.layers.6.mlp.fc2",
165
+ "vision_model.transformer.layers.6.self_attn.out_proj",
166
+ "vision_model.transformer.layers.6.self_attn.qkv_proj",
167
+ "vision_model.transformer.layers.7.mlp.fc1",
168
+ "vision_model.transformer.layers.7.mlp.fc2",
169
+ "vision_model.transformer.layers.7.self_attn.out_proj",
170
+ "vision_model.transformer.layers.7.self_attn.qkv_proj",
171
+ "vision_model.transformer.layers.8.mlp.fc1",
172
+ "vision_model.transformer.layers.8.mlp.fc2",
173
+ "vision_model.transformer.layers.8.self_attn.out_proj",
174
+ "vision_model.transformer.layers.8.self_attn.qkv_proj",
175
+ "vision_model.transformer.layers.9.mlp.fc1",
176
+ "vision_model.transformer.layers.9.mlp.fc2",
177
+ "vision_model.transformer.layers.9.self_attn.out_proj",
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
+ },
195
+ "vision_projector": {
196
+ "status": "success",
197
+ "matched_module_count": 1,
198
+ "matched_modules": [
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",
218
+ "matched_module_count": 1,
219
+ "matched_modules": [
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
+ },
237
+ "attention_q_proj": {
238
+ "status": "success",
239
+ "matched_module_count": 12,
240
+ "matched_modules": [
241
+ "language_model.model.layers.0.self_attn.q_proj",
242
+ "language_model.model.layers.1.self_attn.q_proj",
243
+ "language_model.model.layers.10.self_attn.q_proj",
244
+ "language_model.model.layers.11.self_attn.q_proj",
245
+ "language_model.model.layers.2.self_attn.q_proj",
246
+ "language_model.model.layers.3.self_attn.q_proj",
247
+ "language_model.model.layers.4.self_attn.q_proj",
248
+ "language_model.model.layers.5.self_attn.q_proj",
249
+ "language_model.model.layers.6.self_attn.q_proj",
250
+ "language_model.model.layers.7.self_attn.q_proj",
251
+ "language_model.model.layers.8.self_attn.q_proj",
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
+ },
269
+ "attention_k_proj": {
270
+ "status": "success",
271
+ "matched_module_count": 12,
272
+ "matched_modules": [
273
+ "language_model.model.layers.0.self_attn.k_proj",
274
+ "language_model.model.layers.1.self_attn.k_proj",
275
+ "language_model.model.layers.10.self_attn.k_proj",
276
+ "language_model.model.layers.11.self_attn.k_proj",
277
+ "language_model.model.layers.2.self_attn.k_proj",
278
+ "language_model.model.layers.3.self_attn.k_proj",
279
+ "language_model.model.layers.4.self_attn.k_proj",
280
+ "language_model.model.layers.5.self_attn.k_proj",
281
+ "language_model.model.layers.6.self_attn.k_proj",
282
+ "language_model.model.layers.7.self_attn.k_proj",
283
+ "language_model.model.layers.8.self_attn.k_proj",
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
+ },
301
+ "attention_v_proj": {
302
+ "status": "success",
303
+ "matched_module_count": 12,
304
+ "matched_modules": [
305
+ "language_model.model.layers.0.self_attn.v_proj",
306
+ "language_model.model.layers.1.self_attn.v_proj",
307
+ "language_model.model.layers.10.self_attn.v_proj",
308
+ "language_model.model.layers.11.self_attn.v_proj",
309
+ "language_model.model.layers.2.self_attn.v_proj",
310
+ "language_model.model.layers.3.self_attn.v_proj",
311
+ "language_model.model.layers.4.self_attn.v_proj",
312
+ "language_model.model.layers.5.self_attn.v_proj",
313
+ "language_model.model.layers.6.self_attn.v_proj",
314
+ "language_model.model.layers.7.self_attn.v_proj",
315
+ "language_model.model.layers.8.self_attn.v_proj",
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",
335
+ "matched_module_count": 12,
336
+ "matched_modules": [
337
+ "language_model.model.layers.0.self_attn.o_proj",
338
+ "language_model.model.layers.1.self_attn.o_proj",
339
+ "language_model.model.layers.10.self_attn.o_proj",
340
+ "language_model.model.layers.11.self_attn.o_proj",
341
+ "language_model.model.layers.2.self_attn.o_proj",
342
+ "language_model.model.layers.3.self_attn.o_proj",
343
+ "language_model.model.layers.4.self_attn.o_proj",
344
+ "language_model.model.layers.5.self_attn.o_proj",
345
+ "language_model.model.layers.6.self_attn.o_proj",
346
+ "language_model.model.layers.7.self_attn.o_proj",
347
+ "language_model.model.layers.8.self_attn.o_proj",
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",
367
+ "matched_module_count": 3,
368
+ "matched_modules": [
369
+ "language_model.model.layers.0.mlp.down_proj",
370
+ "language_model.model.layers.0.mlp.gate_proj",
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",
390
+ "matched_module_count": 33,
391
+ "matched_modules": [
392
+ "language_model.model.layers.1.mlp.shared_experts.down_proj",
393
+ "language_model.model.layers.1.mlp.shared_experts.gate_proj",
394
+ "language_model.model.layers.1.mlp.shared_experts.up_proj",
395
+ "language_model.model.layers.10.mlp.shared_experts.down_proj",
396
+ "language_model.model.layers.10.mlp.shared_experts.gate_proj",
397
+ "language_model.model.layers.10.mlp.shared_experts.up_proj",
398
+ "language_model.model.layers.11.mlp.shared_experts.down_proj",
399
+ "language_model.model.layers.11.mlp.shared_experts.gate_proj",
400
+ "language_model.model.layers.11.mlp.shared_experts.up_proj",
401
+ "language_model.model.layers.2.mlp.shared_experts.down_proj",
402
+ "language_model.model.layers.2.mlp.shared_experts.gate_proj",
403
+ "language_model.model.layers.2.mlp.shared_experts.up_proj",
404
+ "language_model.model.layers.3.mlp.shared_experts.down_proj",
405
+ "language_model.model.layers.3.mlp.shared_experts.gate_proj",
406
+ "language_model.model.layers.3.mlp.shared_experts.up_proj",
407
+ "language_model.model.layers.4.mlp.shared_experts.down_proj",
408
+ "language_model.model.layers.4.mlp.shared_experts.gate_proj",
409
+ "language_model.model.layers.4.mlp.shared_experts.up_proj",
410
+ "language_model.model.layers.5.mlp.shared_experts.down_proj",
411
+ "language_model.model.layers.5.mlp.shared_experts.gate_proj",
412
+ "language_model.model.layers.5.mlp.shared_experts.up_proj",
413
+ "language_model.model.layers.6.mlp.shared_experts.down_proj",
414
+ "language_model.model.layers.6.mlp.shared_experts.gate_proj",
415
+ "language_model.model.layers.6.mlp.shared_experts.up_proj",
416
+ "language_model.model.layers.7.mlp.shared_experts.down_proj",
417
+ "language_model.model.layers.7.mlp.shared_experts.gate_proj",
418
+ "language_model.model.layers.7.mlp.shared_experts.up_proj",
419
+ "language_model.model.layers.8.mlp.shared_experts.down_proj",
420
+ "language_model.model.layers.8.mlp.shared_experts.gate_proj",
421
+ "language_model.model.layers.8.mlp.shared_experts.up_proj",
422
+ "language_model.model.layers.9.mlp.shared_experts.down_proj",
423
+ "language_model.model.layers.9.mlp.shared_experts.gate_proj",
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",
443
+ "matched_module_count": 33,
444
+ "matched_modules": [
445
+ "language_model.model.layers.1.mlp.switch_mlp.down_proj",
446
+ "language_model.model.layers.1.mlp.switch_mlp.gate_proj",
447
+ "language_model.model.layers.1.mlp.switch_mlp.up_proj",
448
+ "language_model.model.layers.10.mlp.switch_mlp.down_proj",
449
+ "language_model.model.layers.10.mlp.switch_mlp.gate_proj",
450
+ "language_model.model.layers.10.mlp.switch_mlp.up_proj",
451
+ "language_model.model.layers.11.mlp.switch_mlp.down_proj",
452
+ "language_model.model.layers.11.mlp.switch_mlp.gate_proj",
453
+ "language_model.model.layers.11.mlp.switch_mlp.up_proj",
454
+ "language_model.model.layers.2.mlp.switch_mlp.down_proj",
455
+ "language_model.model.layers.2.mlp.switch_mlp.gate_proj",
456
+ "language_model.model.layers.2.mlp.switch_mlp.up_proj",
457
+ "language_model.model.layers.3.mlp.switch_mlp.down_proj",
458
+ "language_model.model.layers.3.mlp.switch_mlp.gate_proj",
459
+ "language_model.model.layers.3.mlp.switch_mlp.up_proj",
460
+ "language_model.model.layers.4.mlp.switch_mlp.down_proj",
461
+ "language_model.model.layers.4.mlp.switch_mlp.gate_proj",
462
+ "language_model.model.layers.4.mlp.switch_mlp.up_proj",
463
+ "language_model.model.layers.5.mlp.switch_mlp.down_proj",
464
+ "language_model.model.layers.5.mlp.switch_mlp.gate_proj",
465
+ "language_model.model.layers.5.mlp.switch_mlp.up_proj",
466
+ "language_model.model.layers.6.mlp.switch_mlp.down_proj",
467
+ "language_model.model.layers.6.mlp.switch_mlp.gate_proj",
468
+ "language_model.model.layers.6.mlp.switch_mlp.up_proj",
469
+ "language_model.model.layers.7.mlp.switch_mlp.down_proj",
470
+ "language_model.model.layers.7.mlp.switch_mlp.gate_proj",
471
+ "language_model.model.layers.7.mlp.switch_mlp.up_proj",
472
+ "language_model.model.layers.8.mlp.switch_mlp.down_proj",
473
+ "language_model.model.layers.8.mlp.switch_mlp.gate_proj",
474
+ "language_model.model.layers.8.mlp.switch_mlp.up_proj",
475
+ "language_model.model.layers.9.mlp.switch_mlp.down_proj",
476
+ "language_model.model.layers.9.mlp.switch_mlp.gate_proj",
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",
496
+ "matched_module_count": 1,
497
+ "matched_modules": [
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
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ {
4
+ "content": "<|User|>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false
9
+ },
10
+ {
11
+ "content": "<|Assistant|>",
12
+ "lstrip": false,
13
+ "normalized": false,
14
+ "rstrip": false,
15
+ "single_word": false
16
+ }
17
+ ],
18
+ "bos_token": {
19
+ "content": "<|begin▁of▁sentence|>",
20
+ "lstrip": false,
21
+ "normalized": false,
22
+ "rstrip": false,
23
+ "single_word": false
24
+ },
25
+ "eos_token": {
26
+ "content": "<|end▁of▁sentence|>",
27
+ "lstrip": false,
28
+ "normalized": false,
29
+ "rstrip": false,
30
+ "single_word": false
31
+ },
32
+ "pad_token": {
33
+ "content": "<|▁pad▁|>",
34
+ "lstrip": false,
35
+ "normalized": false,
36
+ "rstrip": false,
37
+ "single_word": false
38
+ }
39
+ }
src/unlimited_ocr/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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",
16
+ "__version__",
17
+ ]
src/unlimited_ocr/adapter_registry.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "1.0",
3
+ "description": "LoRA adapter registry for Unlimited-OCR",
4
+ "adapters": {
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
17
+ }
src/unlimited_ocr/cli.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Command-line interface for Unlimited-OCR MLX."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ from unlimited_ocr import __version__
11
+ 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)
18
+
19
+ PROFILE_NAMES = list(PROFILES.keys())
20
+
21
+
22
+ def _resolve_adapter(name_or_path: str) -> str:
23
+ """Resolve an adapter name to a path using the registry."""
24
+ import json
25
+
26
+ # If it's already a path that exists, use directly
27
+ p = Path(name_or_path).expanduser()
28
+ if p.is_dir():
29
+ return str(p)
30
+ if p.exists():
31
+ raise click.ClickException(f"Adapter path is not a directory: {p}")
32
+
33
+ # Try registry lookup
34
+ registry_paths = (
35
+ Path(__file__).with_name("adapter_registry.json"),
36
+ Path(__file__).parent.parent.parent / "adapters" / "registry.json",
37
+ )
38
+ registry_path = next((path for path in registry_paths if path.is_file()), None)
39
+ if registry_path is not None:
40
+ try:
41
+ registry = json.loads(registry_path.read_text(encoding="utf-8"))
42
+ except (OSError, UnicodeError, json.JSONDecodeError) as exc:
43
+ raise click.ClickException(
44
+ f"Cannot read adapter registry: {registry_path}"
45
+ ) from exc
46
+ adapters = registry.get("adapters", {})
47
+ if name_or_path in adapters:
48
+ entry = adapters[name_or_path]
49
+ if not isinstance(entry, dict):
50
+ raise click.ClickException(
51
+ f"Invalid registry entry for adapter '{name_or_path}'"
52
+ )
53
+ relative_path = entry.get("path")
54
+ adapter_path = (
55
+ Path(__file__).parent.parent.parent / relative_path
56
+ if isinstance(relative_path, str)
57
+ else None
58
+ )
59
+ if adapter_path is not None and adapter_path.is_dir():
60
+ return str(adapter_path)
61
+ # Try HF download
62
+ hf_repo = entry.get("hf_repo")
63
+ if hf_repo:
64
+ try:
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(
72
+ f"Could not download adapter '{name_or_path}' from {hf_repo}: {exc}"
73
+ ) from exc
74
+ raise click.ClickException(
75
+ f"Adapter '{name_or_path}' has no local files or download repository"
76
+ )
77
+
78
+ raise click.ClickException(
79
+ f"Unknown adapter or missing adapter directory: {name_or_path}"
80
+ )
81
+
82
+
83
+ @click.group(invoke_without_command=True)
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:
94
+ unlimited-ocr run image.jpg
95
+ unlimited-ocr run document.pdf --format markdown -o output.md
96
+ unlimited-ocr run ./scans/ --format json --preprocess -o results/
97
+ unlimited-ocr run image.jpg --grounding --format json
98
+ """
99
+ if ctx.invoked_subcommand is None:
100
+ click.echo(ctx.get_help())
101
+
102
+
103
+ @main.command()
104
+ @click.argument("input_path", type=click.Path(exists=True))
105
+ @click.option("-o", "--output", "output_path", type=click.Path(), default=None,
106
+ help="Output file or directory path.")
107
+ @click.option("-f", "--format", "fmt", type=click.Choice(["text", "markdown", "json", "structured"]),
108
+ default="text", help="Output format.")
109
+ @click.option("-t", "--task", type=click.Choice([
110
+ "document", "markdown", "figure", "free", "multi_page",
111
+ ]),
112
+ default="document", help="OCR task type.")
113
+ @click.option("-p", "--profile", type=click.Choice(PROFILE_NAMES),
114
+ default="accurate", help="Generation profile.")
115
+ @click.option("--grounding", is_flag=True, default=False,
116
+ help="Enable bounding box detection.")
117
+ @click.option("--preprocess", is_flag=True, default=False,
118
+ help="Enable image preprocessing.")
119
+ @click.option("--preprocess-mode", type=click.Choice(PREPROCESS_MODES),
120
+ default="document", help="Preprocessing mode.")
121
+ @click.option("--dpi", type=click.IntRange(min=1), default=DEFAULT_PDF_DPI,
122
+ show_default=True,
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.
141
+ """
142
+ from unlimited_ocr.pipeline import OCRPipeline
143
+
144
+ input_path = Path(input_path)
145
+
146
+ if stream:
147
+ if input_path.is_dir():
148
+ raise click.UsageError("--stream only supports a single image")
149
+ if output_path is not None:
150
+ raise click.UsageError("--stream writes to stdout and cannot be used with --output")
151
+ if fmt != "text":
152
+ raise click.UsageError("--stream only supports --format text")
153
+ if input_path.suffix.lower() == ".pdf":
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:
166
+ status_console.print(f"[dim]Preprocessing:[/] {preprocess_mode}")
167
+ if adapter:
168
+ status_console.print(f"[dim]Adapter:[/] {adapter}")
169
+ status_console.print()
170
+
171
+ pipeline = None
172
+ try:
173
+ # Resolve adapter path from registry if name given
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)
191
+ else:
192
+ # Single file mode (image or PDF)
193
+ _run_single(pipeline, input_path, output_path, fmt, task,
194
+ grounding, dpi, max_tokens, profile)
195
+ except click.ClickException:
196
+ raise
197
+ except Exception as exc:
198
+ if verbose:
199
+ raise
200
+ raise click.ClickException(str(exc)) from exc
201
+ finally:
202
+ if pipeline is not None:
203
+ pipeline.cleanup()
204
+
205
+
206
+ def _run_stream(pipeline, input_path, task, grounding, dpi, max_tokens, profile):
207
+ """Handle streaming output for a single file."""
208
+ import sys
209
+ for chunk, result in pipeline.run_stream(
210
+ input_path=input_path,
211
+ task=task,
212
+ grounding=grounding,
213
+ dpi=dpi,
214
+ max_tokens=max_tokens,
215
+ profile=profile,
216
+ ):
217
+ if result is None:
218
+ sys.stdout.write(chunk)
219
+ sys.stdout.flush()
220
+ else:
221
+ sys.stdout.write("\n")
222
+ sys.stdout.flush()
223
+
224
+
225
+ def _run_single(pipeline, input_path, output_path, fmt, task, grounding, dpi, max_tokens, profile):
226
+ """Handle single file (image or PDF) processing."""
227
+ result = pipeline.run(
228
+ input_path=input_path,
229
+ task=task,
230
+ format=fmt,
231
+ grounding=grounding,
232
+ dpi=dpi,
233
+ max_tokens=max_tokens,
234
+ output_path=output_path,
235
+ profile=profile,
236
+ )
237
+
238
+ if output_path:
239
+ status_console.print(f"[green]Result saved to:[/] {output_path}")
240
+ else:
241
+ # Print to stdout
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,
249
+ task=task,
250
+ format=fmt,
251
+ grounding=grounding,
252
+ max_tokens=max_tokens,
253
+ output_dir=output_path,
254
+ show_progress=True,
255
+ profile=profile,
256
+ raise_on_error=True,
257
+ jobs=jobs,
258
+ )
259
+
260
+ if output_path:
261
+ status_console.print(f"\n[green]Processed {len(results)} file(s) → {output_path}[/]")
262
+ elif fmt in {"json", "structured"}:
263
+ import json
264
+
265
+ click.echo(json.dumps(
266
+ {filename: json.loads(output) for filename, output in results.items()},
267
+ indent=2,
268
+ ensure_ascii=False,
269
+ ))
270
+ else:
271
+ # Print all results to stdout
272
+ for filename, text in results.items():
273
+ click.echo(f"\n═══ {filename} ═══\n")
274
+ click.echo(text)
275
+
276
+
277
+ @main.command()
278
+ @click.argument("pdf_path", type=click.Path(exists=True))
279
+ def info(pdf_path):
280
+ """Show information about a PDF file (page count, sizes, metadata)."""
281
+ from unlimited_ocr.pdf import get_pdf_info
282
+
283
+ try:
284
+ pdf_info = get_pdf_info(pdf_path)
285
+ except Exception as exc:
286
+ raise click.ClickException(str(exc)) from exc
287
+ console.print(f"[bold]PDF Info:[/] {pdf_path}")
288
+ console.print(f" Pages: {pdf_info['num_pages']}")
289
+
290
+ if pdf_info["page_sizes"]:
291
+ first = pdf_info["page_sizes"][0]
292
+ console.print(f" Page size: {first['width']:.0f} × {first['height']:.0f} pts")
293
+
294
+ meta = pdf_info.get("metadata", {})
295
+ if meta.get("title"):
296
+ console.print(f" Title: {meta['title']}")
297
+ if meta.get("author"):
298
+ console.print(f" Author: {meta['author']}")
299
+
300
+
301
+ @main.command()
302
+ def models():
303
+ """Show available model information."""
304
+ console.print("[bold]Available Models:[/]\n")
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
+
311
+ @main.command()
312
+ def profiles():
313
+ """Show available generation profiles."""
314
+ from unlimited_ocr.profiles import list_profiles
315
+
316
+ console.print("[bold]Generation Profiles:[/]\n")
317
+ for p in list_profiles():
318
+ console.print(f" [green]•[/] [bold]{p['name']}[/]")
319
+ console.print(f" {p['description']}")
320
+ console.print(f" [dim]max_tokens={p['max_tokens']}, temp={p['temperature']}, "
321
+ f"ngram={p['no_repeat_ngram_size']}[/]")
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.")
328
+ @click.option("--port", type=click.IntRange(1, 65535), default=8000,
329
+ show_default=True, help="Bind port.")
330
+ @click.option("--model-path", type=str, default=DEFAULT_MODEL,
331
+ help="Model path (HF repo ID or local directory).")
332
+ @click.option("--verbose", is_flag=True, default=False,
333
+ help="Enable verbose output.")
334
+ def serve(host, port, model_path, verbose):
335
+ """Start the HTTP API server.
336
+
337
+ Requires: pip install unlimited-ocr[server]
338
+
339
+ \b
340
+ Endpoints:
341
+ POST /ocr — Run OCR on uploaded image/PDF
342
+ POST /ocr/stream — SSE streaming OCR
343
+ GET /health — Model status
344
+ """
345
+ try:
346
+ from unlimited_ocr.server import run_server
347
+ except ImportError as exc:
348
+ raise click.ClickException(
349
+ "Server dependencies not installed. Run: pip install unlimited-ocr[server]"
350
+ ) from exc
351
+
352
+ console.print(f"[bold]Starting Unlimited-OCR server[/] on {host}:{port}")
353
+ console.print(f"[dim]Model:[/] {model_path}")
354
+ console.print("[dim]Endpoints:[/] POST /ocr, POST /ocr/stream, GET /health")
355
+ console.print()
356
+
357
+ if not host.strip():
358
+ raise click.UsageError("--host must not be empty")
359
+ try:
360
+ run_server(host=host, port=port, model_path=model_path, verbose=verbose)
361
+ except Exception as exc:
362
+ if verbose:
363
+ raise
364
+ raise click.ClickException(str(exc)) from exc
365
+
366
+
367
+ if __name__ == "__main__":
368
+ main()
src/unlimited_ocr/engine.py ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core MLX-VLM inference engine for Unlimited-OCR."""
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
11
+ from typing import Optional, Union
12
+
13
+ from PIL import Image
14
+ from rich.console import Console
15
+
16
+ from unlimited_ocr.profiles import GenerationProfile, get_profile
17
+
18
+ console = Console(stderr=True)
19
+
20
+ # Paths for disk images; PIL for in-memory preprocessed / PDF-rendered pages.
21
+ ImageInput = Union[str, Path, Image.Image]
22
+
23
+ DEFAULT_MODEL = "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
24
+
25
+ # Prompt templates following DeepSeek-OCR / Unlimited-OCR vocabulary
26
+ PROMPTS = {
27
+ "document": "<image>document parsing.",
28
+ "markdown": "<image>Convert the document to markdown.",
29
+ "figure": "<image>Parse the figure.",
30
+ "free": "<image>Free OCR.",
31
+ "multi_page": "<image>Multi page parsing.",
32
+ }
33
+
34
+ GROUNDING_PREFIX = "<|grounding|>"
35
+
36
+
37
+ @dataclass
38
+ class OCRResult:
39
+ """Result from a single OCR inference."""
40
+
41
+ text: str
42
+ prompt_used: str
43
+ image_path: str
44
+ tokens_generated: int = 0
45
+ grounding: bool = False
46
+ bboxes: list[dict] = field(default_factory=list)
47
+ # Metadata
48
+ elapsed_seconds: float = 0.0
49
+ profile_name: str = "accurate"
50
+ model_path: str = DEFAULT_MODEL
51
+ preprocessing_applied: list[str] = field(default_factory=list)
52
+ generation_tps: float = 0.0
53
+ peak_memory_gb: float = 0.0
54
+
55
+ def __post_init__(self):
56
+ if self.grounding and self.text and not self.bboxes:
57
+ self.bboxes = parse_grounding(self.text)
58
+
59
+ def metadata(self) -> dict:
60
+ """Return inference metadata as a dict."""
61
+ return {
62
+ "model": self.model_path,
63
+ "profile": self.profile_name,
64
+ "preprocessing": self.preprocessing_applied,
65
+ "output_tokens": self.tokens_generated,
66
+ "elapsed_seconds": round(self.elapsed_seconds, 2),
67
+ "generation_tps": round(self.generation_tps, 1),
68
+ "peak_memory_gb": round(self.peak_memory_gb, 2),
69
+ "grounding": self.grounding,
70
+ }
71
+
72
+
73
+ @dataclass
74
+ class EngineConfig:
75
+ """Configuration for the OCR engine."""
76
+
77
+ model_path: str = DEFAULT_MODEL
78
+ # ``None`` means to use the selected profile. Explicit values are
79
+ # constructor-level overrides that apply to every inference call.
80
+ max_tokens: Optional[int] = None
81
+ temperature: Optional[float] = None
82
+ verbose: bool = False
83
+ profile: Optional[str] = "accurate"
84
+ # No-repeat ngram suppression (ported from upstream)
85
+ no_repeat_ngram_size: Optional[int] = None
86
+ ngram_window: int = 128
87
+ # LoRA adapter path (None = no adapter)
88
+ adapter_path: Optional[str] = None
89
+
90
+ def get_generation_profile(self) -> GenerationProfile:
91
+ """Resolve the active generation profile."""
92
+ return get_profile(self.profile)
93
+
94
+
95
+ def parse_grounding(text: str) -> list[dict]:
96
+ """Parse bounding boxes from grounding output.
97
+
98
+ Unlimited-OCR grounding format (after token decoding):
99
+ <|det|>label [x1, y1, x2, y2]<|/det|>content
100
+ The sentencepiece space token (U+0120 / Ġ) may appear as separator.
101
+ Coordinates are in 0-1000 normalized space.
102
+ The first detection block may lack an opening <|det|> tag.
103
+ Blocks are separated by Ċ (newline token).
104
+ """
105
+ # Normalize tokenizer-visible whitespace markers.
106
+ normalized = text.replace("\u0120", " ").replace("Ċ", "\n")
107
+ number = r"-?\d+(?:\.\d+)?"
108
+ coordinate_pattern = re.compile(
109
+ rf"\[\s*({number})\s*,\s*({number})\s*,\s*"
110
+ rf"({number})\s*,\s*({number})\s*\]"
111
+ )
112
+
113
+ def coordinate_from_match(match: re.Match) -> list[int | float]:
114
+ values = [float(match.group(i)) for i in range(1, 5)]
115
+ return [int(value) if value.is_integer() else value for value in values]
116
+
117
+ parsed: list[tuple[int, dict]] = []
118
+
119
+ # Upstream also emits the older ref/det form, whose det span can contain
120
+ # one box or a list of boxes.
121
+ ref_pattern = re.compile(
122
+ r"<\|ref\|>(.*?)<\|/ref\|>\s*<\|det\|>(.*?)<\|/det\|>",
123
+ re.DOTALL,
124
+ )
125
+ ref_spans: list[tuple[int, int]] = []
126
+ for match in ref_pattern.finditer(normalized):
127
+ ref_spans.append(match.span())
128
+ for coordinate_match in coordinate_pattern.finditer(match.group(2)):
129
+ parsed.append((match.start(), {
130
+ "label": match.group(1).strip(),
131
+ "bbox": coordinate_from_match(coordinate_match),
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]
156
+
157
+
158
+ class SlidingWindowNoRepeatNGramProcessor:
159
+ """Block repeated n-grams within a bounded recent-token window.
160
+
161
+ ``mlx-vlm`` accepts custom logits processors but does not expose a native
162
+ no-repeat-n-gram option. This mirrors Unlimited-OCR's upstream safeguard
163
+ without importing MLX at module import time.
164
+ """
165
+
166
+ def __init__(self, ngram_size: int, window_size: int):
167
+ if not isinstance(ngram_size, int) or isinstance(ngram_size, bool) or ngram_size < 1:
168
+ raise ValueError("ngram_size must be a positive integer")
169
+ if not isinstance(window_size, int) or isinstance(window_size, bool) or window_size < 1:
170
+ raise ValueError("window_size must be a positive integer")
171
+ if window_size < ngram_size:
172
+ raise ValueError("window_size must be at least as large as ngram_size")
173
+ self.ngram_size = ngram_size
174
+ self.window_size = window_size
175
+
176
+ def __call__(self, tokens, logits):
177
+ sequence = tokens.tolist() if hasattr(tokens, "tolist") else list(tokens)
178
+ if sequence and isinstance(sequence[0], list):
179
+ sequence = sequence[0]
180
+ if len(sequence) < self.ngram_size:
181
+ return logits
182
+
183
+ prefix_length = self.ngram_size - 1
184
+ current_prefix = tuple(sequence[-prefix_length:]) if prefix_length else ()
185
+ search_start = max(0, len(sequence) - self.window_size)
186
+ search_end = len(sequence) - self.ngram_size + 1
187
+ banned: set[int] = set()
188
+ for index in range(search_start, search_end):
189
+ ngram = sequence[index:index + self.ngram_size]
190
+ if not prefix_length or tuple(ngram[:-1]) == current_prefix:
191
+ banned.add(ngram[-1])
192
+
193
+ if banned:
194
+ logits[:, list(banned)] = float("-inf")
195
+ return logits
196
+
197
+
198
+ class OCREngine:
199
+ """Core inference engine wrapping mlx-vlm for Unlimited-OCR.
200
+
201
+ Uses lazy loading — the model is loaded on first inference call.
202
+ """
203
+
204
+ def __init__(self, config: Optional[EngineConfig] = None):
205
+ self.config = config or EngineConfig()
206
+ self._model = None
207
+ self._processor = None
208
+
209
+ @property
210
+ def model(self):
211
+ """Lazy-load the MLX model."""
212
+ if self._model is None:
213
+ self._load_model()
214
+ return self._model
215
+
216
+ @property
217
+ def processor(self):
218
+ """Lazy-load the processor."""
219
+ if self._processor is None:
220
+ self._load_model()
221
+ return self._processor
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
+
230
+ load_kwargs = {}
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:
240
+ console.print("[bold green]Model loaded successfully.[/]")
241
+
242
+ def build_prompt(
243
+ self,
244
+ task: str = "document",
245
+ grounding: bool = False,
246
+ custom_prompt: Optional[str] = None,
247
+ ) -> str:
248
+ """Build the prompt string for inference.
249
+
250
+ Args:
251
+ task: One of 'document', 'markdown', 'figure', 'free', 'multi_page'.
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,
284
+ tokens: int,
285
+ temperature: float,
286
+ ngram_size: int,
287
+ ) -> None:
288
+ """Validate resolved decoding settings before importing MLX."""
289
+ if not isinstance(tokens, int) or isinstance(tokens, bool) or tokens < 1:
290
+ raise ValueError("max_tokens must be a positive integer")
291
+ if (
292
+ not isinstance(temperature, Real)
293
+ or isinstance(temperature, bool)
294
+ or not math.isfinite(float(temperature))
295
+ or temperature < 0
296
+ ):
297
+ raise ValueError("temperature must be non-negative and finite")
298
+ if (
299
+ not isinstance(ngram_size, int)
300
+ or isinstance(ngram_size, bool)
301
+ or ngram_size < 0
302
+ ):
303
+ raise ValueError("no_repeat_ngram_size must be a non-negative integer")
304
+ if (
305
+ not isinstance(self.config.ngram_window, int)
306
+ or isinstance(self.config.ngram_window, bool)
307
+ or self.config.ngram_window < 1
308
+ ):
309
+ raise ValueError("ngram_window must be a positive integer")
310
+ if ngram_size > self.config.ngram_window:
311
+ raise ValueError(
312
+ "ngram_window must be at least as large as no_repeat_ngram_size"
313
+ )
314
+
315
+ @staticmethod
316
+ def _resolve_image_input(image: ImageInput) -> tuple[Image.Image | str, str]:
317
+ """Normalize image input for mlx-vlm and metadata.
318
+
319
+ Returns:
320
+ (payload for mlx-vlm, path label for OCRResult.image_path)
321
+ """
322
+ if isinstance(image, Image.Image):
323
+ # Copy so callers can free their original buffer independently.
324
+ return image.copy(), "<in-memory>"
325
+ if isinstance(image, (str, Path)):
326
+ path = str(image)
327
+ return path, path
328
+ raise TypeError(
329
+ f"image must be a path or PIL.Image, got {type(image).__name__}"
330
+ )
331
+
332
+ def _resolve_generation(
333
+ self,
334
+ task: str,
335
+ grounding: bool,
336
+ custom_prompt: Optional[str],
337
+ max_tokens: Optional[int],
338
+ profile: Optional[str],
339
+ ) -> tuple[str, bool, GenerationProfile, dict]:
340
+ """Build prompt and generation kwargs shared by infer / infer_stream."""
341
+ prompt = self.build_prompt(task=task, grounding=grounding, custom_prompt=custom_prompt)
342
+ effective_grounding = grounding or GROUNDING_PREFIX in prompt
343
+
344
+ selected_profile = profile if profile is not None else self.config.profile
345
+ gen_profile = get_profile(selected_profile)
346
+ if max_tokens is not None:
347
+ tokens = max_tokens
348
+ elif self.config.max_tokens is not None:
349
+ tokens = self.config.max_tokens
350
+ else:
351
+ tokens = gen_profile.max_tokens
352
+ temperature = (
353
+ self.config.temperature
354
+ if self.config.temperature is not None
355
+ else gen_profile.temperature
356
+ )
357
+ ngram_size = (
358
+ self.config.no_repeat_ngram_size
359
+ if self.config.no_repeat_ngram_size is not None
360
+ else gen_profile.no_repeat_ngram_size
361
+ )
362
+ self._validate_generation_settings(tokens, temperature, ngram_size)
363
+
364
+ gen_kwargs = {
365
+ "max_tokens": tokens,
366
+ "temperature": temperature,
367
+ "top_p": gen_profile.top_p,
368
+ "verbose": self.config.verbose,
369
+ }
370
+ if gen_profile.repetition_penalty > 1.0:
371
+ gen_kwargs["repetition_penalty"] = gen_profile.repetition_penalty
372
+ if ngram_size > 0:
373
+ gen_kwargs["logits_processors"] = [
374
+ SlidingWindowNoRepeatNGramProcessor(ngram_size, self.config.ngram_window)
375
+ ]
376
+ return prompt, effective_grounding, gen_profile, gen_kwargs
377
+
378
+ def infer(
379
+ self,
380
+ image_path: ImageInput,
381
+ task: str = "document",
382
+ grounding: bool = False,
383
+ custom_prompt: Optional[str] = None,
384
+ max_tokens: Optional[int] = None,
385
+ profile: Optional[str] = None,
386
+ preprocessing_applied: Optional[list[str]] = None,
387
+ ) -> OCRResult:
388
+ """Run OCR inference on a single image.
389
+
390
+ Args:
391
+ image_path: Path to the image file, or an in-memory PIL Image.
392
+ task: OCR task type (document, markdown, figure, free, multi_page).
393
+ grounding: Enable bounding box output.
394
+ custom_prompt: Custom prompt override.
395
+ max_tokens: Override max tokens for this inference.
396
+ profile: Override generation profile for this call.
397
+ preprocessing_applied: List of preprocessing steps applied to the image.
398
+
399
+ Returns:
400
+ OCRResult with extracted text and metadata.
401
+ """
402
+ image_payload, image_label = self._resolve_image_input(image_path)
403
+ prompt, effective_grounding, gen_profile, gen_kwargs = self._resolve_generation(
404
+ task, grounding, custom_prompt, max_tokens, profile
405
+ )
406
+
407
+ if self.config.verbose:
408
+ console.print(f"[dim]Profile:[/] {gen_profile.name} | [dim]Max tokens:[/] {gen_kwargs['max_tokens']}")
409
+ console.print(f"[dim]Prompt:[/] {prompt}")
410
+ console.print(f"[dim]Image:[/] {image_label}")
411
+
412
+ # Import only after validation so invalid calls do not initialize Metal.
413
+ from mlx_vlm import generate
414
+
415
+ # Resolve lazy model loading before starting the per-inference timer.
416
+ model = self.model
417
+ processor = self.processor
418
+ start_time = time.perf_counter()
419
+
420
+ response = generate(
421
+ model,
422
+ processor,
423
+ prompt=prompt,
424
+ image=[image_payload],
425
+ **gen_kwargs,
426
+ )
427
+
428
+ elapsed = time.perf_counter() - start_time
429
+
430
+ # mlx-vlm generate returns a GenerationResult object
431
+ if isinstance(response, str):
432
+ text = response
433
+ num_tokens = 0
434
+ gen_tps = 0.0
435
+ peak_mem = 0.0
436
+ elif hasattr(response, "text"):
437
+ text = response.text
438
+ num_tokens = getattr(
439
+ response,
440
+ "generation_tokens",
441
+ getattr(response, "tokens_generated", 0),
442
+ ) or 0
443
+ gen_tps = getattr(response, "generation_tps", 0.0) or 0.0
444
+ peak_mem = getattr(response, "peak_memory", 0.0) or 0.0
445
+ else:
446
+ raise TypeError(
447
+ "mlx-vlm returned an unsupported generation response "
448
+ f"({type(response).__name__})"
449
+ )
450
+
451
+ if not isinstance(text, str):
452
+ raise TypeError("mlx-vlm returned a non-string text field")
453
+
454
+ return OCRResult(
455
+ text=text,
456
+ prompt_used=prompt,
457
+ image_path=image_label,
458
+ tokens_generated=num_tokens,
459
+ grounding=effective_grounding,
460
+ elapsed_seconds=elapsed,
461
+ profile_name=gen_profile.name,
462
+ model_path=self.config.model_path,
463
+ preprocessing_applied=list(preprocessing_applied or []),
464
+ generation_tps=gen_tps,
465
+ peak_memory_gb=peak_mem,
466
+ )
467
+
468
+ def infer_stream(
469
+ self,
470
+ image_path: ImageInput,
471
+ task: str = "document",
472
+ grounding: bool = False,
473
+ custom_prompt: Optional[str] = None,
474
+ max_tokens: Optional[int] = None,
475
+ profile: Optional[str] = None,
476
+ preprocessing_applied: Optional[list[str]] = None,
477
+ ):
478
+ """Run OCR inference with token-by-token streaming.
479
+
480
+ Yields (chunk_text, is_final) tuples. The final yield contains
481
+ the complete OCRResult as the second element.
482
+
483
+ Usage:
484
+ for chunk, result in engine.infer_stream(image):
485
+ print(chunk, end="") # incremental text
486
+ if result is not None:
487
+ # result is the final OCRResult
488
+ pass
489
+ """
490
+ image_payload, image_label = self._resolve_image_input(image_path)
491
+ prompt, effective_grounding, gen_profile, gen_kwargs = self._resolve_generation(
492
+ task, grounding, custom_prompt, max_tokens, profile
493
+ )
494
+ # Streaming should not print token noise to the console.
495
+ gen_kwargs["verbose"] = False
496
+
497
+ import mlx_vlm
498
+
499
+ model = self.model
500
+ processor = self.processor
501
+ start_time = time.perf_counter()
502
+
503
+ full_text = ""
504
+ num_tokens = 0
505
+ gen_tps = 0.0
506
+ peak_mem = 0.0
507
+
508
+ for response in mlx_vlm.stream_generate(
509
+ model,
510
+ processor,
511
+ prompt=prompt,
512
+ image=[image_payload],
513
+ **gen_kwargs,
514
+ ):
515
+ if isinstance(response, str):
516
+ chunk = response
517
+ elif hasattr(response, "text"):
518
+ chunk = response.text
519
+ else:
520
+ raise TypeError(
521
+ "mlx-vlm returned an unsupported stream response "
522
+ f"({type(response).__name__})"
523
+ )
524
+ if not isinstance(chunk, str):
525
+ raise TypeError("mlx-vlm returned a non-string stream chunk")
526
+ # mlx-vlm's stream contract yields only the detokenizer's latest
527
+ # segment. Treating a chunk as cumulative when it happens to share
528
+ # a prefix with prior output loses repeated text (for example two
529
+ # consecutive identical tokens).
530
+ full_text += chunk
531
+ if chunk:
532
+ yield chunk, None
533
+
534
+ # Capture final stats from last response
535
+ if hasattr(response, "generation_tokens"):
536
+ num_tokens = response.generation_tokens or 0
537
+ if hasattr(response, "generation_tps"):
538
+ gen_tps = response.generation_tps or 0.0
539
+ if hasattr(response, "peak_memory"):
540
+ peak_mem = response.peak_memory or 0.0
541
+
542
+ elapsed = time.perf_counter() - start_time
543
+
544
+ result = OCRResult(
545
+ text=full_text,
546
+ prompt_used=prompt,
547
+ image_path=image_label,
548
+ tokens_generated=num_tokens,
549
+ grounding=effective_grounding,
550
+ elapsed_seconds=elapsed,
551
+ profile_name=gen_profile.name,
552
+ model_path=self.config.model_path,
553
+ preprocessing_applied=list(preprocessing_applied or []),
554
+ generation_tps=gen_tps,
555
+ peak_memory_gb=peak_mem,
556
+ )
557
+ yield "", result
558
+
559
+ def is_loaded(self) -> bool:
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
566
+ self._processor = None
src/unlimited_ocr/output.py ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Output formatters for OCR results (text, markdown, JSON, structured)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from html.parser import HTMLParser
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from unlimited_ocr.engine import OCRResult
14
+
15
+
16
+ @dataclass
17
+ class PageResult:
18
+ """OCR result for a single page/image."""
19
+
20
+ page_number: int
21
+ image_path: str
22
+ text: str
23
+ bboxes: list[dict] = field(default_factory=list)
24
+ tokens_generated: int = 0
25
+ generation_tps: float = 0.0
26
+ peak_memory_gb: float = 0.0
27
+
28
+
29
+ @dataclass
30
+ class DocumentResult:
31
+ """Aggregated OCR result for a full document (multi-page)."""
32
+
33
+ source: str
34
+ pages: list[PageResult] = field(default_factory=list)
35
+ grounding: bool = False
36
+ format: str = "text"
37
+ timestamp: str = field(
38
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
39
+ )
40
+ # Inference metadata
41
+ model_path: str = ""
42
+ profile_name: str = "accurate"
43
+ preprocessing_applied: list[str] = field(default_factory=list)
44
+ total_elapsed_seconds: float = 0.0
45
+
46
+ @property
47
+ def full_text(self) -> str:
48
+ """Concatenated text from all pages."""
49
+ return "\n\n".join(p.text for p in self.pages)
50
+
51
+ @property
52
+ def num_pages(self) -> int:
53
+ return len(self.pages)
54
+
55
+ @classmethod
56
+ def from_ocr_results(
57
+ cls,
58
+ results: list[OCRResult],
59
+ source: str,
60
+ grounding: bool = False,
61
+ ) -> "DocumentResult":
62
+ """Create a DocumentResult from a list of OCRResult objects."""
63
+ pages = [
64
+ PageResult(
65
+ page_number=i + 1,
66
+ image_path=r.image_path,
67
+ text=r.text,
68
+ bboxes=r.bboxes,
69
+ tokens_generated=r.tokens_generated,
70
+ generation_tps=r.generation_tps,
71
+ peak_memory_gb=r.peak_memory_gb,
72
+ )
73
+ for i, r in enumerate(results)
74
+ ]
75
+ # Aggregate metadata from all pages. Auto preprocessing can choose
76
+ # different steps for different images.
77
+ model_path = results[0].model_path if results else ""
78
+ profile_name = results[0].profile_name if results else "accurate"
79
+ preprocessing = list(dict.fromkeys(
80
+ step
81
+ for result in results
82
+ for step in result.preprocessing_applied
83
+ ))
84
+ total_elapsed = sum(r.elapsed_seconds for r in results)
85
+
86
+ return cls(
87
+ source=source,
88
+ pages=pages,
89
+ grounding=grounding or any(result.grounding for result in results),
90
+ model_path=model_path, profile_name=profile_name,
91
+ preprocessing_applied=preprocessing,
92
+ total_elapsed_seconds=total_elapsed,
93
+ )
94
+
95
+
96
+ def format_text(result: DocumentResult) -> str:
97
+ """Format as plain text with page separators."""
98
+ if result.num_pages == 1:
99
+ return result.pages[0].text
100
+
101
+ parts = []
102
+ for page in result.pages:
103
+ parts.append(f"--- Page {page.page_number} ---\n\n{page.text}")
104
+ return "\n\n".join(parts)
105
+
106
+
107
+ def format_markdown(result: DocumentResult) -> str:
108
+ """Format as Markdown with page headings."""
109
+ parts = []
110
+ source_name = Path(result.source).stem
111
+
112
+ if result.num_pages > 1:
113
+ parts.append(f"# {source_name}\n")
114
+
115
+ for page in result.pages:
116
+ if result.num_pages > 1:
117
+ parts.append(f"## Page {page.page_number}\n")
118
+ parts.append(page.text)
119
+
120
+ return "\n".join(parts)
121
+
122
+
123
+ def format_json(result: DocumentResult, indent: int = 2) -> str:
124
+ """Format as structured JSON with metadata and optional bounding boxes."""
125
+ output = {
126
+ "source": result.source,
127
+ "num_pages": result.num_pages,
128
+ "grounding": result.grounding,
129
+ "timestamp": result.timestamp,
130
+ "metadata": {
131
+ "model": result.model_path,
132
+ "profile": result.profile_name,
133
+ "preprocessing": result.preprocessing_applied,
134
+ "total_elapsed_seconds": round(result.total_elapsed_seconds, 2),
135
+ },
136
+ "pages": [],
137
+ }
138
+
139
+ for page in result.pages:
140
+ page_data = {
141
+ "page_number": page.page_number,
142
+ "image_path": page.image_path,
143
+ "text": page.text,
144
+ "tokens_generated": page.tokens_generated,
145
+ "generation_tps": round(page.generation_tps, 1),
146
+ "peak_memory_gb": round(page.peak_memory_gb, 2),
147
+ }
148
+ if result.grounding and page.bboxes:
149
+ page_data["bounding_boxes"] = page.bboxes
150
+ output["pages"].append(page_data)
151
+
152
+ return json.dumps(output, indent=indent, ensure_ascii=False)
153
+
154
+
155
+ @dataclass
156
+ class TextBlock:
157
+ """A block of plain text content."""
158
+
159
+ text: str
160
+ block_type: str = "paragraph" # paragraph, heading, list_item, caption
161
+ order: int = 0
162
+ bbox: Optional[list] = None
163
+
164
+
165
+ @dataclass
166
+ class TableBlock:
167
+ """A parsed table extracted from OCR output."""
168
+
169
+ html: str
170
+ rows: list[list[str]] = field(default_factory=list)
171
+ order: int = 0
172
+ bbox: Optional[list] = None
173
+
174
+ @property
175
+ def num_rows(self) -> int:
176
+ return len(self.rows)
177
+
178
+ @property
179
+ def num_cols(self) -> int:
180
+ return max((len(r) for r in self.rows), default=0)
181
+
182
+ def to_csv(self, delimiter: str = ",") -> str:
183
+ """Convert table to CSV string."""
184
+ import csv
185
+ import io
186
+ buf = io.StringIO()
187
+ writer = csv.writer(buf, delimiter=delimiter)
188
+ for row in self.rows:
189
+ writer.writerow(row)
190
+ return buf.getvalue()
191
+
192
+
193
+ @dataclass
194
+ class StructuredPage:
195
+ """Structured representation of a page with separated content types."""
196
+
197
+ page_number: int
198
+ text_blocks: list[TextBlock] = field(default_factory=list)
199
+ tables: list[TableBlock] = field(default_factory=list)
200
+ raw_text: str = ""
201
+ reading_order: list[dict] = field(default_factory=list)
202
+
203
+ @property
204
+ def plain_text(self) -> str:
205
+ """Reconstruct plain text in reading order."""
206
+ parts = []
207
+ for item in sorted(
208
+ [(b.order, b.text) for b in self.text_blocks]
209
+ + [(t.order, t.html) for t in self.tables],
210
+ key=lambda x: x[0],
211
+ ):
212
+ parts.append(item[1])
213
+ return "\n\n".join(parts) if parts else self.raw_text
214
+
215
+
216
+ def parse_tables_from_text(text: str) -> tuple[list[TextBlock], list[TableBlock]]:
217
+ """Parse OCR text into text blocks and table blocks.
218
+
219
+ Detects HTML <table>...</table> regions and separates them from
220
+ surrounding text content.
221
+ """
222
+ text_blocks: list[TextBlock] = []
223
+ table_blocks: list[TableBlock] = []
224
+ order = 0
225
+
226
+ def add_text_blocks(value: str) -> None:
227
+ nonlocal order
228
+ for block_text in re.split(r"\n\s*\n", value):
229
+ block_text = block_text.strip()
230
+ if block_text:
231
+ text_blocks.append(TextBlock(
232
+ text=block_text,
233
+ block_type=_classify_text_block(block_text),
234
+ order=order,
235
+ ))
236
+ order += 1
237
+
238
+ cursor = 0
239
+ for start, end in _html_table_spans(text):
240
+ add_text_blocks(text[cursor:start])
241
+ table_html = text[start:end]
242
+ table_blocks.append(TableBlock(
243
+ html=table_html,
244
+ rows=_parse_html_table(table_html),
245
+ order=order,
246
+ ))
247
+ order += 1
248
+ cursor = end
249
+ add_text_blocks(text[cursor:])
250
+
251
+ return text_blocks, table_blocks
252
+
253
+
254
+ def _html_table_spans(text: str) -> list[tuple[int, int]]:
255
+ """Locate balanced outer HTML table regions, including nested tables."""
256
+ tag_pattern = re.compile(r"</?table\b[^>]*>", re.IGNORECASE)
257
+ spans: list[tuple[int, int]] = []
258
+ depth = 0
259
+ start: Optional[int] = None
260
+ for match in tag_pattern.finditer(text):
261
+ closing = bool(re.match(r"<\s*/", match.group(0)))
262
+ if closing:
263
+ if depth == 0:
264
+ continue
265
+ depth -= 1
266
+ if depth == 0 and start is not None:
267
+ spans.append((start, match.end()))
268
+ start = None
269
+ else:
270
+ if depth == 0:
271
+ start = match.start()
272
+ depth += 1
273
+ return spans
274
+
275
+
276
+ class _TableHTMLParser(HTMLParser):
277
+ """Extract first-level table rows and cells from forgiving HTML."""
278
+
279
+ def __init__(self):
280
+ super().__init__(convert_charrefs=True)
281
+ self.table_depth = 0
282
+ self.rows: list[list[str]] = []
283
+ self._row: Optional[list[str]] = None
284
+ self._cell_parts: Optional[list[str]] = None
285
+
286
+ @staticmethod
287
+ def _clean_cell(parts: list[str]) -> str:
288
+ lines = [" ".join(line.split()) for line in "".join(parts).split("\n")]
289
+ return "\n".join(line for line in lines if line).strip()
290
+
291
+ def _finish_cell(self) -> None:
292
+ if self._cell_parts is None:
293
+ return
294
+ if self._row is None:
295
+ self._row = []
296
+ self._row.append(self._clean_cell(self._cell_parts))
297
+ self._cell_parts = None
298
+
299
+ def _finish_row(self) -> None:
300
+ self._finish_cell()
301
+ if self._row:
302
+ self.rows.append(self._row)
303
+ self._row = None
304
+
305
+ def handle_starttag(self, tag: str, attrs) -> None:
306
+ tag = tag.lower()
307
+ if tag == "table":
308
+ self.table_depth += 1
309
+ return
310
+ if self.table_depth != 1:
311
+ return
312
+ if tag == "tr":
313
+ self._finish_row()
314
+ self._row = []
315
+ elif tag in {"td", "th"}:
316
+ self._finish_cell()
317
+ if self._row is None:
318
+ self._row = []
319
+ self._cell_parts = []
320
+ elif tag == "br" and self._cell_parts is not None:
321
+ self._cell_parts.append("\n")
322
+
323
+ def handle_endtag(self, tag: str) -> None:
324
+ tag = tag.lower()
325
+ if tag == "table":
326
+ if self.table_depth == 1:
327
+ self._finish_row()
328
+ if self.table_depth > 0:
329
+ self.table_depth -= 1
330
+ return
331
+ if self.table_depth != 1:
332
+ return
333
+ if tag in {"td", "th"}:
334
+ self._finish_cell()
335
+ elif tag == "tr":
336
+ self._finish_row()
337
+
338
+ def handle_data(self, data: str) -> None:
339
+ if self.table_depth >= 1 and self._cell_parts is not None:
340
+ self._cell_parts.append(data)
341
+
342
+
343
+ def _parse_html_table(html: str) -> list[list[str]]:
344
+ """Extract rows from an HTML table string."""
345
+ parser = _TableHTMLParser()
346
+ parser.feed(html)
347
+ parser.close()
348
+ parser._finish_row()
349
+ return parser.rows
350
+
351
+
352
+ def _classify_text_block(text: str) -> str:
353
+ """Heuristically classify a text block type."""
354
+ lines = text.split("\n")
355
+ first_line = lines[0].strip()
356
+
357
+ # Markdown heading
358
+ if first_line.startswith("#"):
359
+ return "heading"
360
+ # List items
361
+ if all(
362
+ line.strip().startswith(("-", "•", "*")) or re.match(r"^\d+[.)]\s", line.strip())
363
+ for line in lines if line.strip()
364
+ ):
365
+ return "list_item"
366
+ # Short single line (likely caption or heading)
367
+ if len(lines) == 1 and len(first_line) < 80 and not first_line.endswith("."):
368
+ return "heading"
369
+ return "paragraph"
370
+
371
+
372
+ def format_structured(result: DocumentResult, indent: int = 2) -> str:
373
+ """Format as structured JSON with separated text blocks and tables."""
374
+ output = {
375
+ "source": result.source,
376
+ "num_pages": result.num_pages,
377
+ "grounding": result.grounding,
378
+ "timestamp": result.timestamp,
379
+ "metadata": {
380
+ "model": result.model_path,
381
+ "profile": result.profile_name,
382
+ "preprocessing": result.preprocessing_applied,
383
+ "total_elapsed_seconds": round(result.total_elapsed_seconds, 2),
384
+ },
385
+ "pages": [],
386
+ }
387
+
388
+ for page in result.pages:
389
+ text_blocks, table_blocks = parse_tables_from_text(page.text)
390
+
391
+ page_data = {
392
+ "page_number": page.page_number,
393
+ "image_path": page.image_path,
394
+ "raw_text": page.text,
395
+ "tokens_generated": page.tokens_generated,
396
+ "generation_tps": round(page.generation_tps, 1),
397
+ "peak_memory_gb": round(page.peak_memory_gb, 2),
398
+ "text_blocks": [
399
+ {
400
+ "type": b.block_type,
401
+ "text": b.text,
402
+ "order": b.order,
403
+ **({"bbox": b.bbox} if b.bbox else {}),
404
+ }
405
+ for b in text_blocks
406
+ ],
407
+ "tables": [
408
+ {
409
+ "html": t.html,
410
+ "rows": t.rows,
411
+ "num_rows": t.num_rows,
412
+ "num_cols": t.num_cols,
413
+ "order": t.order,
414
+ **({"bbox": t.bbox} if t.bbox else {}),
415
+ }
416
+ for t in table_blocks
417
+ ],
418
+ "reading_order": sorted(
419
+ [
420
+ {"kind": "text", "index": index, "order": block.order}
421
+ for index, block in enumerate(text_blocks)
422
+ ]
423
+ + [
424
+ {"kind": "table", "index": index, "order": table.order}
425
+ for index, table in enumerate(table_blocks)
426
+ ],
427
+ key=lambda item: item["order"],
428
+ ),
429
+ }
430
+ if result.grounding and page.bboxes:
431
+ page_data["bounding_boxes"] = page.bboxes
432
+ output["pages"].append(page_data)
433
+
434
+ return json.dumps(output, indent=indent, ensure_ascii=False)
435
+
436
+
437
+ def format_result(result: DocumentResult, fmt: str = "text") -> str:
438
+ """Format a DocumentResult in the specified format.
439
+
440
+ Args:
441
+ result: The document OCR result.
442
+ fmt: One of 'text', 'markdown', 'json', 'structured'.
443
+
444
+ Returns:
445
+ Formatted string output.
446
+ """
447
+ formatters = {
448
+ "text": format_text,
449
+ "markdown": format_markdown,
450
+ "json": format_json,
451
+ "structured": format_structured,
452
+ }
453
+
454
+ formatter = formatters.get(fmt)
455
+ if formatter is None:
456
+ raise ValueError(f"Unknown format '{fmt}'. Choose from: {list(formatters.keys())}")
457
+
458
+ return formatter(result)
459
+
460
+
461
+ def save_result(
462
+ result: DocumentResult,
463
+ output_path: str | Path,
464
+ fmt: Optional[str] = None,
465
+ ) -> Path:
466
+ """Format and save a DocumentResult to a file.
467
+
468
+ Args:
469
+ result: The document OCR result.
470
+ output_path: Path to write the output file.
471
+ fmt: Output format. If None, inferred from file extension.
472
+
473
+ Returns:
474
+ Path to the saved file.
475
+ """
476
+ output_path = Path(output_path)
477
+
478
+ if fmt is None:
479
+ ext = output_path.suffix.lower()
480
+ fmt_map = {
481
+ ".txt": "text",
482
+ ".md": "markdown",
483
+ ".json": "json",
484
+ }
485
+ fmt = fmt_map.get(ext, "text")
486
+
487
+ content = format_result(result, fmt)
488
+
489
+ output_path.parent.mkdir(parents=True, exist_ok=True)
490
+ output_path.write_text(content, encoding="utf-8")
491
+
492
+ return output_path