Image-Text-to-Text
MLX
Safetensors
unlimited-ocr
ax-engine
mlx-vlm
ocr
mxfp8
int8
apple-silicon
automatosx
conversational
8-bit precision
Instructions to use AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 with MLX:
# Make sure mlx-vlm is installed # pip install --upgrade mlx-vlm from mlx_vlm import load, generate from mlx_vlm.prompt_utils import apply_chat_template from mlx_vlm.utils import load_config # Load the model model, processor = load("AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8") config = load_config("AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8") # Prepare input image = ["http://images.cocodataset.org/val2017/000000039769.jpg"] prompt = "Describe this image." # Apply chat template formatted_prompt = apply_chat_template( processor, config, prompt, num_images=1 ) # Generate output output = generate(model, processor, formatted_prompt, image) print(output) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Publish verified OCR-aware MXFP8 checkpoint
Browse files- README.md +81 -27
- benchmarks/datasets.md +36 -1
- benchmarks/evaluate_tables.py +1 -2
- benchmarks/rswa_validation.py +83 -23
- benchmarks/run_accuracy.py +241 -33
- benchmarks/run_performance.py +55 -5
- config.json +0 -10
- docs/PRD.md +2 -0
- docs/TECHNICAL_SPEC.md +70 -4
- docs/adr/0002-calibrated-affine8-lm-head.md +9 -3
- examples/single_image.py +1 -1
- model.safetensors +2 -2
- model.safetensors.index.json +1 -2
- precision_map.json +5 -5
- preprocessing_config.json +24 -1
- pyproject.toml +16 -7
- quantization/calibrate_precision.py +169 -41
- quantization/layer_sensitivity.py +21 -9
- quantization/mixed_precision_convert.py +40 -3
- quantization/release_gate.py +1714 -160
- quantization/run_pipeline.py +299 -26
- quantization_config.json +9 -8
- quantization_summary.json +8 -9
- release/bf16_accuracy.json +128 -72
- release/calibration_affine8_accuracy.json +221 -0
- release/calibration_affine8_performance.json +69 -0
- release/calibration_baseline_accuracy.json +221 -0
- release/calibration_bfloat16_accuracy.json +221 -0
- release/calibration_bfloat16_performance.json +69 -0
- release/calibration_mxfp8_accuracy.json +221 -0
- release/calibration_mxfp8_performance.json +69 -0
- release/calibration_reference_performance.json +69 -0
- release/calibration_results.json +187 -26
- release/candidate_accuracy.json +133 -77
- release/candidate_performance.json +24 -23
- release/candidate_rswa.json +21 -16
- release/generated_precision_map.json +5 -5
- release/provenance.json +162 -43
- release/reference_accuracy.json +137 -81
- release/reference_performance.json +18 -17
- release/release_manifest.json +1163 -263
- release/sensitivity_results.json +221 -103
- src/unlimited_ocr/__init__.py +5 -2
- src/unlimited_ocr/adapter_registry.json +10 -2
- src/unlimited_ocr/cli.py +151 -14
- src/unlimited_ocr/engine.py +120 -38
- src/unlimited_ocr/pdf.py +47 -40
- src/unlimited_ocr/pipeline.py +122 -28
- src/unlimited_ocr/preprocessing.py +10 -0
- tokenizer_config.json +1 -1
README.md
CHANGED
|
@@ -10,9 +10,13 @@ tags:
|
|
| 10 |
- mxfp8
|
| 11 |
- int8
|
| 12 |
- apple-silicon
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
# AX Unlimited-OCR 3B MoE — MLX MXFP8
|
| 16 |
|
| 17 |
This is an AutomatosX sensitivity-guided MLX conversion of
|
| 18 |
[`baidu/Unlimited-OCR`](https://huggingface.co/baidu/Unlimited-OCR). It was
|
|
@@ -21,6 +25,14 @@ created from the upstream BF16 weights. The
|
|
| 21 |
checkpoint was used only as a comparison reference; these are not repackaged
|
| 22 |
Sahil weights.
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
## What was optimized
|
| 25 |
|
| 26 |
Each supported module group was quantized independently and evaluated for OCR
|
|
@@ -28,41 +40,74 @@ CER, digit CER, and table structure. Eligible language linear layers use MXFP8
|
|
| 28 |
(8 data bits, group size 32). A joint calibration then selected affine INT8
|
| 29 |
(group size 32) for the LM head because BF16 missed the throughput floor and
|
| 30 |
MXFP8 missed the CER ceiling. OCR-critical or sensitive groups remain BF16.
|
| 31 |
-
Retained groups: vision_encoder.
|
| 32 |
|
| 33 |
-
The model uses mlx-vlm's native `model_type=unlimited-ocr` implementation
|
| 34 |
-
sliding-window size of
|
|
|
|
|
|
|
| 35 |
|
| 36 |
## AutomatosX validation
|
| 37 |
|
| 38 |
-
These are local release measurements, not upstream paper benchmark claims
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
(12 with digits, 3 with
|
| 41 |
CJK, and 3 with tables), digest
|
| 42 |
-
`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 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 |
-
|
| 51 |
-
|
|
|
|
| 52 |
|
| 53 |
## Release gates
|
| 54 |
|
| 55 |
-
- ✅ `weights_are_distinct` — actual `
|
| 56 |
-
- ✅ `weight_size_gb` — actual `3.
|
| 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 |
-
- ✅ `
|
| 60 |
-
- ✅ `
|
| 61 |
-
- ✅ `
|
| 62 |
-
- ✅ `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
- ✅ `candidate_table_score_vs_bf16` — actual `0.0`, limit `0.01`
|
| 64 |
-
- ✅ `
|
| 65 |
-
- ✅ `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
Full per-sample evidence, sensitivity results, precision rules, PRD, ADR, and
|
| 68 |
technical specification are included under `release/`, `quantization/`, and
|
|
@@ -70,8 +115,11 @@ technical specification are included under `release/`, `quantization/`, and
|
|
| 70 |
|
| 71 |
## Usage
|
| 72 |
|
|
|
|
|
|
|
|
|
|
| 73 |
```bash
|
| 74 |
-
pip install mlx-vlm
|
| 75 |
python -m mlx_vlm.generate \
|
| 76 |
--model AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
|
| 77 |
--image document.png \
|
|
@@ -79,6 +127,12 @@ python -m mlx_vlm.generate \
|
|
| 79 |
--max-tokens 4096
|
| 80 |
```
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
- mxfp8
|
| 11 |
- int8
|
| 12 |
- apple-silicon
|
| 13 |
+
widget:
|
| 14 |
+
- text: "<image>document parsing."
|
| 15 |
+
- text: "<image>Convert the document to markdown."
|
| 16 |
+
- text: "<image>Free OCR. Extract all text."
|
| 17 |
---
|
| 18 |
|
| 19 |
+
# AX Unlimited-OCR 3B MoE — MLX (MXFP8 + affine INT8 head)
|
| 20 |
|
| 21 |
This is an AutomatosX sensitivity-guided MLX conversion of
|
| 22 |
[`baidu/Unlimited-OCR`](https://huggingface.co/baidu/Unlimited-OCR). It was
|
|
|
|
| 25 |
checkpoint was used only as a comparison reference; these are not repackaged
|
| 26 |
Sahil weights.
|
| 27 |
|
| 28 |
+
**Hybrid precision (name is shorthand):** MXFP8 (group size 32) on eligible
|
| 29 |
+
language linears, **affine INT8** on the LM head, **BF16** on the vision
|
| 30 |
+
encoder and other protected groups. The Hub “1B params” style size reflects
|
| 31 |
+
active MoE capacity; the marketing name refers to the upstream 3B MoE family.
|
| 32 |
+
|
| 33 |
+
Immutable upstream source revision: `ee63731b6461c8afcdcc7b15352e7d2ffecc2ead`.
|
| 34 |
+
Reference checkpoint revision: `55b8031a6c867de675279d9604e38cc94b9882a4`.
|
| 35 |
+
|
| 36 |
## What was optimized
|
| 37 |
|
| 38 |
Each supported module group was quantized independently and evaluated for OCR
|
|
|
|
| 40 |
(8 data bits, group size 32). A joint calibration then selected affine INT8
|
| 41 |
(group size 32) for the LM head because BF16 missed the throughput floor and
|
| 42 |
MXFP8 missed the CER ceiling. OCR-critical or sensitive groups remain BF16.
|
| 43 |
+
Retained groups: vision_encoder, vision_projector.
|
| 44 |
|
| 45 |
+
The model uses mlx-vlm's native `model_type=unlimited-ocr` implementation,
|
| 46 |
+
`processor_class=UnlimitedOCRHFProcessor`, and a sliding-window size of
|
| 47 |
+
128. Prompts should be raw Unlimited-OCR strings such as
|
| 48 |
+
`<image>document parsing.` (not multi-turn chat formatting).
|
| 49 |
|
| 50 |
## AutomatosX validation
|
| 51 |
|
| 52 |
+
These are **local release measurements**, not upstream paper benchmark claims
|
| 53 |
+
and **not** OmniDocBench / olmOCR-Bench scores. Absolute CER on this set is
|
| 54 |
+
not a production accuracy bar; gates check **parity with BF16** and packaging
|
| 55 |
+
integrity.
|
| 56 |
+
|
| 57 |
+
The content-addressed held-out evaluation set had 12 samples
|
| 58 |
(12 with digits, 3 with
|
| 59 |
CJK, and 3 with tables), digest
|
| 60 |
+
`7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987`.
|
| 61 |
+
|
| 62 |
+
| Checkpoint | Mean CER | Digit CER | CJK CER | Table score | Decode tok/s |
|
| 63 |
+
|---|---:|---:|---:|---:|---:|
|
| 64 |
+
| Upstream BF16 | 0.205212 | 0.113310 | 0.333333 | 1.000000 | — |
|
| 65 |
+
| Community MXFP8 reference | 0.907381 | 0.406511 | 1.000000 | 1.000000 | 35.07 |
|
| 66 |
+
| AutomatosX candidate | 0.216331 | 0.111939 | 0.341270 | 1.000000 | 35.14 |
|
| 67 |
+
|
| 68 |
+
**Caveats**
|
| 69 |
+
- Small local set; some samples are synthetic. Do not treat mean CER as field accuracy.
|
| 70 |
+
- The community reference may use a different `model_type` / load path; it is a
|
| 71 |
+
throughput and packaging baseline, not an identical-recipe peer.
|
| 72 |
+
- Generation profiles used in AutomatosX tooling live in the `unlimited-ocr`
|
| 73 |
+
package (`profiles.py`); weight-only loads use mlx-vlm defaults unless you
|
| 74 |
+
pass the same decoding settings.
|
| 75 |
+
|
| 76 |
+
### Weight integrity
|
| 77 |
|
| 78 |
+
Weight size: 3.582 GiB
|
| 79 |
+
Aggregate weight SHA-256: `5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549`
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
Per-file weight SHA-256:
|
| 82 |
+
|
| 83 |
+
- `model.safetensors`: `d9fe11fc8d2e333000be6a0e288681231efa69282c844f7775ca40f52db02f0c`
|
| 84 |
|
| 85 |
## Release gates
|
| 86 |
|
| 87 |
+
- ✅ `weights_are_distinct` — actual `5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549`, limit `different from 439c6acfe5e277537dfe3368b94a145a6b0da4c39e4bd00582977d030f292ad1`
|
| 88 |
+
- ✅ `weight_size_gb` — actual `3.5815872186794877`, limit `4.5`
|
| 89 |
+
- ✅ `native_model_metadata` — actual `{'architecture': True, 'model_type': True, 'mxfp8': True, 'sliding_window': True, 'processor_class': True, 'sft_format': True, 'precision_map': True, 'quantization_summary': True, 'protected_modules_preserved': True}`, limit `True`
|
| 90 |
+
- ✅ `candidate_source_provenance` — actual `{'model': 'Unlimited-OCR', 'revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead'}`, limit `{'model': ['baidu/Unlimited-OCR', 'Unlimited-OCR'], 'revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead'}`
|
| 91 |
- ✅ `evaluation_coverage` — actual `{'samples': 12, 'digit': 12, 'cjk': 3, 'table': 3}`, limit `all counts > 0`
|
| 92 |
+
- ✅ `held_out_evaluation_dataset` — actual `{'passed': True, 'calibration_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'evaluation_digest': '7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987', 'overlapping_image_sha256': [], 'overlapping_ground_truth_sha256': []}`, limit `distinct dataset digests and no shared image or ground-truth hashes`
|
| 93 |
+
- ✅ `accuracy_aggregates_recomputed` — actual `[True, True, True]`, limit `[True, True, True]`
|
| 94 |
+
- ✅ `same_evaluation_samples` — actual `{'counts': [12, 12, 12], 'file_counts': [12, 12, 12]}`, limit `{'count': 12, 'files': ['0001.png', '0002.png', '0003.png', '0004.png', '0005.png', '0006.png', '0007.png', '0008.png', '0009.png', '0010.png', '0011.png', '0012.png']}`
|
| 95 |
+
- ✅ `model_identities` — actual `{'bf16': 'Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'candidate': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'reference_performance': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'candidate_performance': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'rswa': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8'}`, limit `{'bf16': 'Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'candidate': 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8'}`
|
| 96 |
+
- ✅ `immutable_model_revisions` — actual `{'bf16_accuracy': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_accuracy': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_accuracy': None, 'reference_performance': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_performance': None}`, limit `{'bf16_accuracy': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_accuracy': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_accuracy': None, 'reference_performance': '55b8031a6c867de675279d9604e38cc94b9882a4', 'candidate_performance': None}`
|
| 97 |
+
- ✅ `same_accuracy_recipe` — actual `{'prompt': ['<image>document parsing.', '<image>document parsing.', '<image>document parsing.'], 'max_tokens': [1024, 1024, 1024], 'profile': ['accurate', 'accurate', 'accurate'], 'generation_settings': [{'temperature': 0.0, 'top_p': 1.0, 'repetition_penalty': 1.0, 'no_repeat_ngram_size': 35, 'ngram_window': 128}, {'temperature': 0.0, 'top_p': 1.0, 'repetition_penalty': 1.0, 'no_repeat_ngram_size': 35, 'ngram_window': 128}, {'temperature': 0.0, 'top_p': 1.0, 'repetition_penalty': 1.0, 'no_repeat_ngram_size': 35, 'ngram_window': 128}]}`, limit `identical official MLX OCR recipe`
|
| 98 |
+
- ✅ `candidate_cer_vs_bf16` — actual `0.011118653228923858`, limit `0.015`
|
| 99 |
+
- ✅ `candidate_cer_vs_reference` — actual `-0.691050362380767`, limit `0.005`
|
| 100 |
+
- ✅ `candidate_digit_cer_vs_bf16` — actual `-0.0013706317359285375`, limit `0.01`
|
| 101 |
- ✅ `candidate_table_score_vs_bf16` — actual `0.0`, limit `0.01`
|
| 102 |
+
- ✅ `performance_aggregates_recomputed` — actual `{'reference': True, 'candidate': True}`, limit `{'reference': True, 'candidate': True}`
|
| 103 |
+
- ✅ `same_performance_setup` — actual `{'image_path': ['test_invoice.png', 'test_invoice.png'], 'prompt': ['<image>document parsing.', '<image>document parsing.'], 'max_tokens': [256, 256], 'num_warmup': [1, 1], 'num_runs': [3, 3], 'system': [{'platform': 'macOS-26.5.2-arm64-arm-64bit-Mach-O', 'processor': 'arm', 'python_version': '3.14.6', 'machine': 'arm64', 'mlx_version': '0.32.0', 'mlx_vlm_version': '0.6.6', 'chip': 'Apple M3 Max', 'total_memory_gb': 128.0}, {'platform': 'macOS-26.5.2-arm64-arm-64bit-Mach-O', 'processor': 'arm', 'python_version': '3.14.6', 'machine': 'arm64', 'mlx_version': '0.32.0', 'mlx_vlm_version': '0.6.6', 'chip': 'Apple M3 Max', 'total_memory_gb': 128.0}]}`, limit `identical setup with at least three complete runs`
|
| 104 |
+
- ✅ `candidate_tps_vs_reference` — actual `1.001955642681865`, limit `0.9`
|
| 105 |
+
- ✅ `rswa_8k_bounded` — actual `{'pass_conditions': {'cache_bounded': True, 'tps_stable': True, '8k_test_passed': True}, 'tokens': 8192, 'repetition_rate': 0.20496894409937888}`, limit `{'min_tokens': 8192, 'max_repetition_rate': 0.25}`
|
| 106 |
+
- ✅ `provenance_matches_release` — actual `{'source': 'baidu/Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'target': 'AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'source_config': {'path': 'config.json', 'sha256': '27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9'}, 'source_revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_revision': '55b8031a6c867de675279d9604e38cc94b9882a4', 'dataset_digest': '7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987', 'calibration_dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb'}`, limit `{'source': 'baidu/Unlimited-OCR', 'reference': 'sahilchachra/unlimited-ocr-mxfp8-mlx', 'target': 'AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8', 'source_config_sha256': '27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9', 'source_revision': 'ee63731b6461c8afcdcc7b15352e7d2ffecc2ead', 'reference_revision': '55b8031a6c867de675279d9604e38cc94b9882a4', 'dataset_digest': '7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987', 'calibration_dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb'}`
|
| 107 |
+
- ✅ `calibration_recomputed` — actual `{'selected': {'label': 'affine8-head', 'precision': 'affine8'}, 'models': ['AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8', 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16', 'AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8'], 'dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'input_artifacts': {'calibration_baseline_accuracy': {'filename': 'calibration_baseline_accuracy.json', 'size': 5680, 'sha256': 'ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6'}, 'calibration_reference_performance': {'filename': 'calibration_reference_performance.json', 'size': 2271, 'sha256': '98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4'}, 'calibration_bfloat16_accuracy': {'filename': 'calibration_bfloat16_accuracy.json', 'size': 5678, 'sha256': '5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0'}, 'calibration_bfloat16_performance': {'filename': 'calibration_bfloat16_performance.json', 'size': 2244, 'sha256': 'dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0'}, 'calibration_mxfp8_accuracy': {'filename': 'calibration_mxfp8_accuracy.json', 'size': 5689, 'sha256': '77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26'}, 'calibration_mxfp8_performance': {'filename': 'calibration_mxfp8_performance.json', 'size': 2252, 'sha256': 'e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b'}, 'calibration_affine8_accuracy': {'filename': 'calibration_affine8_accuracy.json', 'size': 5657, 'sha256': 'c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19'}, 'calibration_affine8_performance': {'filename': 'calibration_affine8_performance.json', 'size': 2247, 'sha256': 'f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f'}}}`, limit `content-addressed inputs and fastest passing experiment`
|
| 108 |
+
- ✅ `sensitivity_matches_calibration_dataset` — actual `{'dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'baseline_metrics_match': True}`, limit `{'dataset_digest': '40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb', 'baseline_metrics_match': True}`
|
| 109 |
+
- ✅ `candidate_precision_map_matches_evidence` — actual `91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75`, limit `91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75`
|
| 110 |
+
- ✅ `precision_map_reproducible` — actual `c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a`, limit `c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a`
|
| 111 |
|
| 112 |
Full per-sample evidence, sensitivity results, precision rules, PRD, ADR, and
|
| 113 |
technical specification are included under `release/`, `quantization/`, and
|
|
|
|
| 115 |
|
| 116 |
## Usage
|
| 117 |
|
| 118 |
+
Requires **Apple Silicon** and **mlx-vlm ≥ 0.6.4** (native Unlimited-OCR / R-SWA).
|
| 119 |
+
Tested stack for this release: mlx-vlm `0.6.6`, mlx `0.32.0`.
|
| 120 |
+
|
| 121 |
```bash
|
| 122 |
+
pip install 'mlx-vlm>=0.6.4'
|
| 123 |
python -m mlx_vlm.generate \
|
| 124 |
--model AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
|
| 125 |
--image document.png \
|
|
|
|
| 127 |
--max-tokens 4096
|
| 128 |
```
|
| 129 |
|
| 130 |
+
Or with the AutomatosX toolkit (deskew, PDF, profiles):
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
pip install 'unlimited-ocr[mlx]'
|
| 134 |
+
# then use OCRPipeline / the unlimited-ocr CLI with model_path=AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
This is quantization/conversion work, not foundation-model retraining or domain
|
| 138 |
+
fine-tuning.
|
benchmarks/datasets.md
CHANGED
|
@@ -35,9 +35,44 @@ The evaluation set must be separate from any calibration data used for quantizat
|
|
| 35 |
benchmarks/eval_data/
|
| 36 |
├── images/ # Input document images (PNG/JPG)
|
| 37 |
├── ground_truth/ # Reference text files (same stem as image)
|
| 38 |
-
└──
|
| 39 |
```
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
### Ground Truth Format
|
| 42 |
|
| 43 |
Each ground truth file is a plain `.txt` file containing the expected OCR output.
|
|
|
|
| 35 |
benchmarks/eval_data/
|
| 36 |
├── images/ # Input document images (PNG/JPG)
|
| 37 |
├── ground_truth/ # Reference text files (same stem as image)
|
| 38 |
+
└── manifest.json # Exact sample paths, categories, and source metadata
|
| 39 |
```
|
| 40 |
|
| 41 |
+
### Calibration vs held-out evaluation (required for schema-3 release)
|
| 42 |
+
|
| 43 |
+
Sensitivity / LM-head calibration and the final release accuracy run must use
|
| 44 |
+
**disjoint** directories. Recommended layout under `eval_dataset/`:
|
| 45 |
+
|
| 46 |
+
```
|
| 47 |
+
eval_dataset/
|
| 48 |
+
├── calibration/
|
| 49 |
+
│ ├── images/
|
| 50 |
+
│ ├── ground_truth/
|
| 51 |
+
│ └── manifest.json
|
| 52 |
+
└── evaluation/ # held-out; used for release_gate accuracy
|
| 53 |
+
├── images/
|
| 54 |
+
├── ground_truth/
|
| 55 |
+
└── manifest.json
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Build with non-overlapping FUNSD IDs and different synthetic seeds:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
python eval_dataset/build_release_set.py --clean \
|
| 62 |
+
--output-root eval_dataset/calibration \
|
| 63 |
+
--funsd-ids 0,1 --generator-seed 20260720 \
|
| 64 |
+
--funsd-revision <SHA> --latin-font <PATH> --cjk-font <PATH>
|
| 65 |
+
|
| 66 |
+
python eval_dataset/build_release_set.py --clean \
|
| 67 |
+
--output-root eval_dataset/evaluation \
|
| 68 |
+
--funsd-ids 2,3,4,5 --generator-seed 20260721 \
|
| 69 |
+
--funsd-revision <SHA> --latin-font <PATH> --cjk-font <PATH>
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
`benchmarks/run_accuracy.py` reports aggregate metrics and, when
|
| 73 |
+
`manifest.json` lists categories, a `by_category` breakdown. Use
|
| 74 |
+
`--save-predictions DIR` to dump hypotheses for failure analysis.
|
| 75 |
+
|
| 76 |
### Ground Truth Format
|
| 77 |
|
| 78 |
Each ground truth file is a plain `.txt` file containing the expected OCR output.
|
benchmarks/evaluate_tables.py
CHANGED
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
| 11 |
|
| 12 |
import re
|
| 13 |
from html.parser import HTMLParser
|
| 14 |
-
from pathlib import Path
|
| 15 |
|
| 16 |
|
| 17 |
def _split_markdown_row(line: str) -> list[str]:
|
|
@@ -178,7 +177,7 @@ def normalize_table(table_text: str) -> dict:
|
|
| 178 |
if re.search(r"<table\b", table_text, re.IGNORECASE):
|
| 179 |
return _normalize_html_table(table_text)
|
| 180 |
|
| 181 |
-
lines = [
|
| 182 |
|
| 183 |
# Filter out the Markdown header separator.
|
| 184 |
data_lines = [line for line in lines if not _is_separator_row(line)]
|
|
|
|
| 11 |
|
| 12 |
import re
|
| 13 |
from html.parser import HTMLParser
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
def _split_markdown_row(line: str) -> list[str]:
|
|
|
|
| 177 |
if re.search(r"<table\b", table_text, re.IGNORECASE):
|
| 178 |
return _normalize_html_table(table_text)
|
| 179 |
|
| 180 |
+
lines = [line.strip() for line in table_text.strip().split("\n") if line.strip()]
|
| 181 |
|
| 182 |
# Filter out the Markdown header separator.
|
| 183 |
data_lines = [line for line in lines if not _is_separator_row(line)]
|
benchmarks/rswa_validation.py
CHANGED
|
@@ -14,13 +14,17 @@ from __future__ import annotations
|
|
| 14 |
|
| 15 |
import argparse
|
| 16 |
import json
|
|
|
|
| 17 |
import platform
|
| 18 |
import sys
|
| 19 |
import time
|
|
|
|
| 20 |
from pathlib import Path
|
| 21 |
|
| 22 |
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 23 |
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# Output lengths to test
|
| 26 |
TEST_LENGTHS = [512, 2048, 4096, 8192, 16384]
|
|
@@ -45,7 +49,9 @@ class MinGeneratedTokensProcessor:
|
|
| 45 |
normalized_ids = sorted({
|
| 46 |
int(token_id)
|
| 47 |
for token_id in eos_token_ids
|
| 48 |
-
if isinstance(token_id,
|
|
|
|
|
|
|
| 49 |
})
|
| 50 |
if not normalized_ids:
|
| 51 |
raise ValueError("At least one integer EOS token ID is required")
|
|
@@ -81,8 +87,12 @@ def collect_eos_token_ids(model, processor) -> list[int]:
|
|
| 81 |
for value in values:
|
| 82 |
candidates = value if isinstance(value, (list, tuple, set)) else [value]
|
| 83 |
for candidate in candidates:
|
| 84 |
-
if
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
if not normalized:
|
| 87 |
raise ValueError("Could not determine EOS token IDs from model or processor")
|
| 88 |
return sorted(normalized)
|
|
@@ -90,10 +100,12 @@ def collect_eos_token_ids(model, processor) -> list[int]:
|
|
| 90 |
|
| 91 |
def detect_repetition_rate(text: str, ngram_size: int = 35, threshold: int = 3) -> float:
|
| 92 |
"""Detect fraction of repeated n-grams in output."""
|
|
|
|
|
|
|
| 93 |
if not isinstance(ngram_size, int) or isinstance(ngram_size, bool) or ngram_size < 1:
|
| 94 |
raise ValueError("ngram_size must be a positive integer")
|
| 95 |
-
if not isinstance(threshold, int) or isinstance(threshold, bool) or threshold <
|
| 96 |
-
raise ValueError("threshold must be a
|
| 97 |
words = text.split()
|
| 98 |
if len(words) < ngram_size:
|
| 99 |
return 0.0
|
|
@@ -111,20 +123,31 @@ def analyze_cache_behavior(results: list[dict]) -> tuple[bool | None, str]:
|
|
| 111 |
Returns ``None`` when the generated token counts do not span enough lengths
|
| 112 |
to distinguish a plateau from ordinary noise or linear growth.
|
| 113 |
"""
|
| 114 |
-
successful = [
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 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 |
-
|
|
|
|
| 128 |
points = sorted(by_tokens.items())
|
| 129 |
|
| 130 |
if len(points) < 3:
|
|
@@ -223,6 +246,8 @@ def run_rswa_validation(
|
|
| 223 |
)
|
| 224 |
|
| 225 |
baseline_memory = mx.get_active_memory() / 1e6
|
|
|
|
|
|
|
| 226 |
print(f"Baseline memory: {baseline_memory:.0f} MB")
|
| 227 |
|
| 228 |
results = []
|
|
@@ -232,6 +257,8 @@ def run_rswa_validation(
|
|
| 232 |
|
| 233 |
mx.clear_cache()
|
| 234 |
mem_before = mx.get_active_memory() / 1e6
|
|
|
|
|
|
|
| 235 |
mx.reset_peak_memory()
|
| 236 |
start = time.perf_counter()
|
| 237 |
|
|
@@ -241,11 +268,13 @@ def run_rswa_validation(
|
|
| 241 |
if force_min_tokens is not None
|
| 242 |
else None
|
| 243 |
)
|
| 244 |
-
logits_processors =
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
| 249 |
response = generate(
|
| 250 |
model, processor,
|
| 251 |
prompt=prompt,
|
|
@@ -261,11 +290,20 @@ def run_rswa_validation(
|
|
| 261 |
if isinstance(response, dict)
|
| 262 |
else getattr(response, "peak_memory", None)
|
| 263 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
mem_after = (
|
| 265 |
float(peak_memory) * 1000
|
| 266 |
-
if peak_memory is not None and
|
| 267 |
else mx.get_peak_memory() / 1e6
|
| 268 |
)
|
|
|
|
|
|
|
| 269 |
|
| 270 |
# Extract text
|
| 271 |
if isinstance(response, str):
|
|
@@ -312,14 +350,31 @@ def run_rswa_validation(
|
|
| 312 |
tokens_reliable = False
|
| 313 |
|
| 314 |
# Compute metrics
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
)
|
| 324 |
repetition = detect_repetition_rate(text)
|
| 325 |
memory_growth = max(0.0, mem_after - mem_before)
|
|
@@ -406,6 +461,11 @@ def run_rswa_validation(
|
|
| 406 |
"image_path": Path(image_path).name,
|
| 407 |
"prompt": prompt,
|
| 408 |
"force_min_tokens": force_min_tokens,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
"forced_eos_token_ids": eos_token_ids,
|
| 410 |
"platform": platform.platform(),
|
| 411 |
"processor": platform.processor(),
|
|
|
|
| 14 |
|
| 15 |
import argparse
|
| 16 |
import json
|
| 17 |
+
import math
|
| 18 |
import platform
|
| 19 |
import sys
|
| 20 |
import time
|
| 21 |
+
from numbers import Integral, Real
|
| 22 |
from pathlib import Path
|
| 23 |
|
| 24 |
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 25 |
|
| 26 |
+
from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor # noqa: E402
|
| 27 |
+
|
| 28 |
|
| 29 |
# Output lengths to test
|
| 30 |
TEST_LENGTHS = [512, 2048, 4096, 8192, 16384]
|
|
|
|
| 49 |
normalized_ids = sorted({
|
| 50 |
int(token_id)
|
| 51 |
for token_id in eos_token_ids
|
| 52 |
+
if isinstance(token_id, Integral)
|
| 53 |
+
and not isinstance(token_id, bool)
|
| 54 |
+
and token_id >= 0
|
| 55 |
})
|
| 56 |
if not normalized_ids:
|
| 57 |
raise ValueError("At least one integer EOS token ID is required")
|
|
|
|
| 87 |
for value in values:
|
| 88 |
candidates = value if isinstance(value, (list, tuple, set)) else [value]
|
| 89 |
for candidate in candidates:
|
| 90 |
+
if (
|
| 91 |
+
isinstance(candidate, Integral)
|
| 92 |
+
and not isinstance(candidate, bool)
|
| 93 |
+
and candidate >= 0
|
| 94 |
+
):
|
| 95 |
+
normalized.add(int(candidate))
|
| 96 |
if not normalized:
|
| 97 |
raise ValueError("Could not determine EOS token IDs from model or processor")
|
| 98 |
return sorted(normalized)
|
|
|
|
| 100 |
|
| 101 |
def detect_repetition_rate(text: str, ngram_size: int = 35, threshold: int = 3) -> float:
|
| 102 |
"""Detect fraction of repeated n-grams in output."""
|
| 103 |
+
if not isinstance(text, str):
|
| 104 |
+
raise TypeError("text must be a string")
|
| 105 |
if not isinstance(ngram_size, int) or isinstance(ngram_size, bool) or ngram_size < 1:
|
| 106 |
raise ValueError("ngram_size must be a positive integer")
|
| 107 |
+
if not isinstance(threshold, int) or isinstance(threshold, bool) or threshold < 1:
|
| 108 |
+
raise ValueError("threshold must be a positive integer")
|
| 109 |
words = text.split()
|
| 110 |
if len(words) < ngram_size:
|
| 111 |
return 0.0
|
|
|
|
| 123 |
Returns ``None`` when the generated token counts do not span enough lengths
|
| 124 |
to distinguish a plateau from ordinary noise or linear growth.
|
| 125 |
"""
|
| 126 |
+
successful = []
|
| 127 |
+
for result in results:
|
| 128 |
+
if not isinstance(result, dict) or result.get("status") != "success":
|
| 129 |
+
continue
|
| 130 |
+
tokens = result.get("tokens_generated")
|
| 131 |
+
memory = result.get("memory_growth_mb")
|
| 132 |
+
if (
|
| 133 |
+
not isinstance(tokens, Integral)
|
| 134 |
+
or isinstance(tokens, bool)
|
| 135 |
+
or tokens <= 0
|
| 136 |
+
or result.get("tokens_generated_reliable", True) is not True
|
| 137 |
+
or not isinstance(memory, Real)
|
| 138 |
+
or isinstance(memory, bool)
|
| 139 |
+
or not math.isfinite(float(memory))
|
| 140 |
+
or memory < 0
|
| 141 |
+
):
|
| 142 |
+
continue
|
| 143 |
+
successful.append(result)
|
| 144 |
# Collapse duplicate actual token counts; raising max_tokens is not useful
|
| 145 |
# evidence when the model naturally stops at the same length each time.
|
| 146 |
by_tokens: dict[int, float] = {}
|
| 147 |
for result in successful:
|
| 148 |
tokens = int(result["tokens_generated"])
|
| 149 |
+
memory = float(result["memory_growth_mb"])
|
| 150 |
+
by_tokens[tokens] = max(by_tokens.get(tokens, 0.0), memory)
|
| 151 |
points = sorted(by_tokens.items())
|
| 152 |
|
| 153 |
if len(points) < 3:
|
|
|
|
| 246 |
)
|
| 247 |
|
| 248 |
baseline_memory = mx.get_active_memory() / 1e6
|
| 249 |
+
if not math.isfinite(baseline_memory) or baseline_memory < 0:
|
| 250 |
+
raise ValueError("MLX returned invalid active memory")
|
| 251 |
print(f"Baseline memory: {baseline_memory:.0f} MB")
|
| 252 |
|
| 253 |
results = []
|
|
|
|
| 257 |
|
| 258 |
mx.clear_cache()
|
| 259 |
mem_before = mx.get_active_memory() / 1e6
|
| 260 |
+
if not math.isfinite(mem_before) or mem_before < 0:
|
| 261 |
+
raise ValueError("MLX returned invalid active memory")
|
| 262 |
mx.reset_peak_memory()
|
| 263 |
start = time.perf_counter()
|
| 264 |
|
|
|
|
| 268 |
if force_min_tokens is not None
|
| 269 |
else None
|
| 270 |
)
|
| 271 |
+
logits_processors = [
|
| 272 |
+
SlidingWindowNoRepeatNGramProcessor(35, 128)
|
| 273 |
+
]
|
| 274 |
+
if forced_minimum is not None:
|
| 275 |
+
logits_processors.append(
|
| 276 |
+
MinGeneratedTokensProcessor(forced_minimum, eos_token_ids)
|
| 277 |
+
)
|
| 278 |
response = generate(
|
| 279 |
model, processor,
|
| 280 |
prompt=prompt,
|
|
|
|
| 290 |
if isinstance(response, dict)
|
| 291 |
else getattr(response, "peak_memory", None)
|
| 292 |
)
|
| 293 |
+
if peak_memory is not None and (
|
| 294 |
+
not isinstance(peak_memory, Real)
|
| 295 |
+
or isinstance(peak_memory, bool)
|
| 296 |
+
or not math.isfinite(float(peak_memory))
|
| 297 |
+
or peak_memory < 0
|
| 298 |
+
):
|
| 299 |
+
raise ValueError("mlx-vlm returned invalid peak_memory")
|
| 300 |
mem_after = (
|
| 301 |
float(peak_memory) * 1000
|
| 302 |
+
if peak_memory is not None and peak_memory > 0
|
| 303 |
else mx.get_peak_memory() / 1e6
|
| 304 |
)
|
| 305 |
+
if not math.isfinite(mem_after) or mem_after < 0:
|
| 306 |
+
raise ValueError("MLX returned invalid peak memory")
|
| 307 |
|
| 308 |
# Extract text
|
| 309 |
if isinstance(response, str):
|
|
|
|
| 350 |
tokens_reliable = False
|
| 351 |
|
| 352 |
# Compute metrics
|
| 353 |
+
if (
|
| 354 |
+
not isinstance(tokens_generated, Integral)
|
| 355 |
+
or isinstance(tokens_generated, bool)
|
| 356 |
+
or not 0 <= tokens_generated <= max_tokens
|
| 357 |
+
):
|
| 358 |
+
raise ValueError("mlx-vlm returned an invalid generation token count")
|
| 359 |
+
tokens_generated = int(tokens_generated)
|
| 360 |
reported_tps = (
|
| 361 |
response.get("generation_tps", 0.0)
|
| 362 |
if isinstance(response, dict)
|
| 363 |
else getattr(response, "generation_tps", 0.0)
|
| 364 |
)
|
| 365 |
+
if isinstance(reported_tps, bool) or (
|
| 366 |
+
reported_tps is not None
|
| 367 |
+
and (
|
| 368 |
+
not isinstance(reported_tps, Real)
|
| 369 |
+
or not math.isfinite(float(reported_tps))
|
| 370 |
+
or reported_tps < 0
|
| 371 |
+
)
|
| 372 |
+
):
|
| 373 |
+
raise ValueError("mlx-vlm returned invalid generation_tps")
|
| 374 |
+
tps = (
|
| 375 |
+
float(reported_tps)
|
| 376 |
+
if reported_tps
|
| 377 |
+
else tokens_generated / elapsed if elapsed > 0 else 0.0
|
| 378 |
)
|
| 379 |
repetition = detect_repetition_rate(text)
|
| 380 |
memory_growth = max(0.0, mem_after - mem_before)
|
|
|
|
| 461 |
"image_path": Path(image_path).name,
|
| 462 |
"prompt": prompt,
|
| 463 |
"force_min_tokens": force_min_tokens,
|
| 464 |
+
"generation_settings": {
|
| 465 |
+
"temperature": 0.0,
|
| 466 |
+
"no_repeat_ngram_size": 35,
|
| 467 |
+
"ngram_window": 128,
|
| 468 |
+
},
|
| 469 |
"forced_eos_token_ids": eos_token_ids,
|
| 470 |
"platform": platform.platform(),
|
| 471 |
"processor": platform.processor(),
|
benchmarks/run_accuracy.py
CHANGED
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
| 12 |
import argparse
|
| 13 |
from collections import Counter
|
| 14 |
import json
|
|
|
|
| 15 |
import sys
|
| 16 |
import time
|
| 17 |
from pathlib import Path
|
|
@@ -22,14 +23,75 @@ PROJECT_ROOT = Path(__file__).parent.parent
|
|
| 22 |
sys.path.insert(0, str(PROJECT_ROOT))
|
| 23 |
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
| 24 |
|
| 25 |
-
from benchmarks.evaluate_cer import (
|
| 26 |
compute_cer, compute_wer, compute_digit_cer,
|
| 27 |
compute_cjk_cer, detect_repetition, extract_cjk_characters,
|
| 28 |
)
|
| 29 |
-
from benchmarks.normalize_output import normalize_ocr_output
|
| 30 |
-
from benchmarks.evaluate_tables import evaluate_tables
|
| 31 |
-
from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor
|
| 32 |
-
from unlimited_ocr.profiles import get_profile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
def run_accuracy_benchmark(
|
|
@@ -38,6 +100,11 @@ def run_accuracy_benchmark(
|
|
| 38 |
prompt: str = "<image>document parsing.",
|
| 39 |
max_tokens: int | None = None,
|
| 40 |
profile: str = "accurate",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
) -> dict:
|
| 42 |
"""Run accuracy benchmark on evaluation dataset.
|
| 43 |
|
|
@@ -60,6 +127,12 @@ def run_accuracy_benchmark(
|
|
| 60 |
token_limit = generation_profile.max_tokens if max_tokens is None else max_tokens
|
| 61 |
if not isinstance(token_limit, int) or isinstance(token_limit, bool) or token_limit < 1:
|
| 62 |
raise ValueError("max_tokens must be a positive integer")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
# Collect image files
|
| 65 |
image_files = sorted(
|
|
@@ -97,12 +170,85 @@ def run_accuracy_benchmark(
|
|
| 97 |
(image_file, gt_dir / f"{image_file.stem}.txt")
|
| 98 |
for image_file in image_files
|
| 99 |
]
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
| 103 |
|
| 104 |
print(f"Loading model: {model_path}")
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
results = []
|
| 108 |
total_time = 0.0
|
|
@@ -112,20 +258,28 @@ def run_accuracy_benchmark(
|
|
| 112 |
|
| 113 |
# Run inference
|
| 114 |
start = time.perf_counter()
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
elapsed = time.perf_counter() - start
|
| 130 |
total_time += elapsed
|
| 131 |
|
|
@@ -135,7 +289,10 @@ def run_accuracy_benchmark(
|
|
| 135 |
elif hasattr(response, "text"):
|
| 136 |
hypothesis = response.text
|
| 137 |
else:
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
hypothesis = normalize_ocr_output(hypothesis)
|
| 140 |
|
| 141 |
# Compute metrics
|
|
@@ -151,8 +308,10 @@ def run_accuracy_benchmark(
|
|
| 151 |
else None
|
| 152 |
)
|
| 153 |
|
|
|
|
| 154 |
result = {
|
| 155 |
"file": img_file.name,
|
|
|
|
| 156 |
"cer": cer,
|
| 157 |
"wer": wer,
|
| 158 |
"digit_cer": digit_cer,
|
|
@@ -169,6 +328,12 @@ def run_accuracy_benchmark(
|
|
| 169 |
}
|
| 170 |
results.append(result)
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
print(f" [{i+1}/{len(evaluation_files)}] {img_file.name}: CER={cer:.4f} "
|
| 173 |
f"DigitCER={digit_cer:.4f} ({elapsed:.1f}s)")
|
| 174 |
|
|
@@ -177,19 +342,30 @@ def run_accuracy_benchmark(
|
|
| 177 |
digit_scores = [r["digit_cer"] for r in results if r["ref_digit_count"] > 0]
|
| 178 |
cjk_scores = [r["cjk_cer"] for r in results if r["ref_cjk_count"] > 0]
|
| 179 |
table_scores = [r["table_score"] for r in results if r["table_score"] is not None]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
summary = {
|
| 181 |
"model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
|
|
|
|
|
|
|
| 182 |
"prompt": prompt,
|
| 183 |
"max_tokens": token_limit,
|
| 184 |
"profile": profile,
|
| 185 |
-
"generation_settings":
|
| 186 |
-
|
| 187 |
-
"top_p": generation_profile.top_p,
|
| 188 |
-
"repetition_penalty": generation_profile.repetition_penalty,
|
| 189 |
-
"no_repeat_ngram_size": generation_profile.no_repeat_ngram_size,
|
| 190 |
-
"ngram_window": 128,
|
| 191 |
-
},
|
| 192 |
-
"num_images": len(image_files),
|
| 193 |
"num_samples": len(results),
|
| 194 |
"total_time_seconds": total_time,
|
| 195 |
"mean_cer": float(np.mean([r["cer"] for r in results])) if results else 0,
|
|
@@ -201,6 +377,7 @@ def run_accuracy_benchmark(
|
|
| 201 |
"num_table_samples": len(table_scores),
|
| 202 |
"mean_table_score": float(np.mean(table_scores)) if table_scores else None,
|
| 203 |
"mean_repetition_rate": float(np.mean([r["repetition_rate"] for r in results])) if results else 0,
|
|
|
|
| 204 |
"per_file": results,
|
| 205 |
}
|
| 206 |
|
|
@@ -217,6 +394,20 @@ def main():
|
|
| 217 |
help="Override the selected profile's token limit")
|
| 218 |
parser.add_argument("--profile", default="accurate",
|
| 219 |
choices=["accurate", "fast", "long-document", "plain-text", "markdown"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
args = parser.parse_args()
|
| 221 |
|
| 222 |
print("=" * 60)
|
|
@@ -229,11 +420,19 @@ def main():
|
|
| 229 |
prompt=args.prompt,
|
| 230 |
max_tokens=args.max_tokens,
|
| 231 |
profile=args.profile,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
)
|
| 233 |
|
| 234 |
# Save results
|
| 235 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 236 |
-
args.output.write_text(
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
def metric(value):
|
| 239 |
return "N/A" if value is None else f"{value:.4f}"
|
|
@@ -242,6 +441,15 @@ def main():
|
|
| 242 |
print(f"Results: CER={results['mean_cer']:.4f} | "
|
| 243 |
f"DigitCER={metric(results['mean_digit_cer'])} | "
|
| 244 |
f"CJK={metric(results['mean_cjk_cer'])}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
print(f"Saved to: {args.output}")
|
| 246 |
|
| 247 |
|
|
|
|
| 12 |
import argparse
|
| 13 |
from collections import Counter
|
| 14 |
import json
|
| 15 |
+
import os
|
| 16 |
import sys
|
| 17 |
import time
|
| 18 |
from pathlib import Path
|
|
|
|
| 23 |
sys.path.insert(0, str(PROJECT_ROOT))
|
| 24 |
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
| 25 |
|
| 26 |
+
from benchmarks.evaluate_cer import ( # noqa: E402
|
| 27 |
compute_cer, compute_wer, compute_digit_cer,
|
| 28 |
compute_cjk_cer, detect_repetition, extract_cjk_characters,
|
| 29 |
)
|
| 30 |
+
from benchmarks.normalize_output import normalize_ocr_output # noqa: E402
|
| 31 |
+
from benchmarks.evaluate_tables import evaluate_tables # noqa: E402
|
| 32 |
+
from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor # noqa: E402
|
| 33 |
+
from unlimited_ocr.profiles import get_profile # noqa: E402
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def load_category_map(eval_dir: Path) -> dict[str, str]:
|
| 37 |
+
"""Map image filenames to categories from ``manifest.json`` when present."""
|
| 38 |
+
manifest_path = Path(eval_dir) / "manifest.json"
|
| 39 |
+
if not manifest_path.is_file():
|
| 40 |
+
return {}
|
| 41 |
+
try:
|
| 42 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 43 |
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
| 44 |
+
return {}
|
| 45 |
+
samples = manifest.get("samples")
|
| 46 |
+
if not isinstance(samples, list):
|
| 47 |
+
return {}
|
| 48 |
+
mapping: dict[str, str] = {}
|
| 49 |
+
for sample in samples:
|
| 50 |
+
if not isinstance(sample, dict):
|
| 51 |
+
continue
|
| 52 |
+
category = sample.get("category")
|
| 53 |
+
image = sample.get("image")
|
| 54 |
+
if not isinstance(category, str) or not category or not isinstance(image, str):
|
| 55 |
+
continue
|
| 56 |
+
mapping[Path(image).name] = category
|
| 57 |
+
return mapping
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def aggregate_by_category(per_file: list[dict]) -> dict[str, dict]:
|
| 61 |
+
"""Aggregate CER-family metrics by category label on each per-file row."""
|
| 62 |
+
buckets: dict[str, list[dict]] = {}
|
| 63 |
+
for row in per_file:
|
| 64 |
+
if not isinstance(row, dict):
|
| 65 |
+
continue
|
| 66 |
+
category = row.get("category")
|
| 67 |
+
if not isinstance(category, str) or not category:
|
| 68 |
+
category = "uncategorized"
|
| 69 |
+
buckets.setdefault(category, []).append(row)
|
| 70 |
+
|
| 71 |
+
summary: dict[str, dict] = {}
|
| 72 |
+
for category, rows in sorted(buckets.items()):
|
| 73 |
+
digit_scores = [r["digit_cer"] for r in rows if r.get("ref_digit_count", 0) > 0]
|
| 74 |
+
cjk_scores = [r["cjk_cer"] for r in rows if r.get("ref_cjk_count", 0) > 0]
|
| 75 |
+
table_scores = [r["table_score"] for r in rows if r.get("table_score") is not None]
|
| 76 |
+
summary[category] = {
|
| 77 |
+
"num_samples": len(rows),
|
| 78 |
+
"mean_cer": (
|
| 79 |
+
sum(r["cer"] for r in rows) / len(rows) if rows else None
|
| 80 |
+
),
|
| 81 |
+
"mean_digit_cer": (
|
| 82 |
+
sum(digit_scores) / len(digit_scores) if digit_scores else None
|
| 83 |
+
),
|
| 84 |
+
"mean_cjk_cer": (
|
| 85 |
+
sum(cjk_scores) / len(cjk_scores) if cjk_scores else None
|
| 86 |
+
),
|
| 87 |
+
"mean_table_score": (
|
| 88 |
+
sum(table_scores) / len(table_scores) if table_scores else None
|
| 89 |
+
),
|
| 90 |
+
"num_digit_samples": len(digit_scores),
|
| 91 |
+
"num_cjk_samples": len(cjk_scores),
|
| 92 |
+
"num_table_samples": len(table_scores),
|
| 93 |
+
}
|
| 94 |
+
return summary
|
| 95 |
|
| 96 |
|
| 97 |
def run_accuracy_benchmark(
|
|
|
|
| 100 |
prompt: str = "<image>document parsing.",
|
| 101 |
max_tokens: int | None = None,
|
| 102 |
profile: str = "accurate",
|
| 103 |
+
backend: str = "mlx",
|
| 104 |
+
vllm_base_url: str = "http://127.0.0.1:8000/v1",
|
| 105 |
+
vllm_api_key: str | None = None,
|
| 106 |
+
served_revision: str | None = None,
|
| 107 |
+
save_predictions: Path | None = None,
|
| 108 |
) -> dict:
|
| 109 |
"""Run accuracy benchmark on evaluation dataset.
|
| 110 |
|
|
|
|
| 127 |
token_limit = generation_profile.max_tokens if max_tokens is None else max_tokens
|
| 128 |
if not isinstance(token_limit, int) or isinstance(token_limit, bool) or token_limit < 1:
|
| 129 |
raise ValueError("max_tokens must be a positive integer")
|
| 130 |
+
if backend not in {"mlx", "vllm"}:
|
| 131 |
+
raise ValueError("backend must be one of: mlx, vllm")
|
| 132 |
+
if served_revision is not None and (
|
| 133 |
+
not isinstance(served_revision, str) or not served_revision.strip()
|
| 134 |
+
):
|
| 135 |
+
raise ValueError("served_revision must be a non-empty string or None")
|
| 136 |
|
| 137 |
# Collect image files
|
| 138 |
image_files = sorted(
|
|
|
|
| 170 |
(image_file, gt_dir / f"{image_file.stem}.txt")
|
| 171 |
for image_file in image_files
|
| 172 |
]
|
| 173 |
+
category_map = load_category_map(eval_dir)
|
| 174 |
+
if save_predictions is not None:
|
| 175 |
+
save_predictions = Path(save_predictions)
|
| 176 |
+
save_predictions.mkdir(parents=True, exist_ok=True)
|
| 177 |
|
| 178 |
print(f"Loading model: {model_path}")
|
| 179 |
+
if backend == "mlx":
|
| 180 |
+
# Import only after validating the dataset and generation options.
|
| 181 |
+
from mlx_vlm import generate as mlx_generate, load
|
| 182 |
+
|
| 183 |
+
model, processor = load(model_path, revision=served_revision)
|
| 184 |
+
return _evaluate_accuracy_files(
|
| 185 |
+
model_path=model_path,
|
| 186 |
+
evaluation_files=evaluation_files,
|
| 187 |
+
prompt=prompt,
|
| 188 |
+
token_limit=token_limit,
|
| 189 |
+
profile=profile,
|
| 190 |
+
backend=backend,
|
| 191 |
+
served_revision=served_revision,
|
| 192 |
+
generation_profile=generation_profile,
|
| 193 |
+
model=model,
|
| 194 |
+
processor=processor,
|
| 195 |
+
generate=mlx_generate,
|
| 196 |
+
engine=None,
|
| 197 |
+
category_map=category_map,
|
| 198 |
+
save_predictions=save_predictions,
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
from unlimited_ocr.vllm_backend import VLLMEngine, VLLMEngineConfig
|
| 202 |
+
|
| 203 |
+
engine = VLLMEngine(
|
| 204 |
+
VLLMEngineConfig(
|
| 205 |
+
model_path=model_path,
|
| 206 |
+
base_url=vllm_base_url,
|
| 207 |
+
api_key=vllm_api_key,
|
| 208 |
+
max_tokens=token_limit,
|
| 209 |
+
profile=profile,
|
| 210 |
+
)
|
| 211 |
+
)
|
| 212 |
+
try:
|
| 213 |
+
engine.ensure_ready()
|
| 214 |
+
return _evaluate_accuracy_files(
|
| 215 |
+
model_path=model_path,
|
| 216 |
+
evaluation_files=evaluation_files,
|
| 217 |
+
prompt=prompt,
|
| 218 |
+
token_limit=token_limit,
|
| 219 |
+
profile=profile,
|
| 220 |
+
backend=backend,
|
| 221 |
+
served_revision=served_revision,
|
| 222 |
+
generation_profile=generation_profile,
|
| 223 |
+
model=None,
|
| 224 |
+
processor=None,
|
| 225 |
+
generate=None,
|
| 226 |
+
engine=engine,
|
| 227 |
+
category_map=category_map,
|
| 228 |
+
save_predictions=save_predictions,
|
| 229 |
+
)
|
| 230 |
+
finally:
|
| 231 |
+
engine.unload()
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _evaluate_accuracy_files(
|
| 235 |
+
*,
|
| 236 |
+
model_path: str,
|
| 237 |
+
evaluation_files: list[tuple[Path, Path]],
|
| 238 |
+
prompt: str,
|
| 239 |
+
token_limit: int,
|
| 240 |
+
profile: str,
|
| 241 |
+
backend: str,
|
| 242 |
+
served_revision: str | None,
|
| 243 |
+
generation_profile,
|
| 244 |
+
model,
|
| 245 |
+
processor,
|
| 246 |
+
generate,
|
| 247 |
+
engine,
|
| 248 |
+
category_map: dict[str, str] | None = None,
|
| 249 |
+
save_predictions: Path | None = None,
|
| 250 |
+
) -> dict:
|
| 251 |
+
"""Evaluate already-loaded MLX or vLLM inference resources."""
|
| 252 |
|
| 253 |
results = []
|
| 254 |
total_time = 0.0
|
|
|
|
| 258 |
|
| 259 |
# Run inference
|
| 260 |
start = time.perf_counter()
|
| 261 |
+
if backend == "mlx":
|
| 262 |
+
response = generate(
|
| 263 |
+
model, processor,
|
| 264 |
+
prompt=prompt,
|
| 265 |
+
image=[str(img_file)],
|
| 266 |
+
max_tokens=token_limit,
|
| 267 |
+
temperature=generation_profile.temperature,
|
| 268 |
+
top_p=generation_profile.top_p,
|
| 269 |
+
repetition_penalty=generation_profile.repetition_penalty,
|
| 270 |
+
logits_processors=[SlidingWindowNoRepeatNGramProcessor(
|
| 271 |
+
generation_profile.no_repeat_ngram_size,
|
| 272 |
+
128,
|
| 273 |
+
)],
|
| 274 |
+
verbose=False,
|
| 275 |
+
)
|
| 276 |
+
else:
|
| 277 |
+
response = engine.infer(
|
| 278 |
+
str(img_file),
|
| 279 |
+
custom_prompt=prompt,
|
| 280 |
+
max_tokens=token_limit,
|
| 281 |
+
profile=profile,
|
| 282 |
+
)
|
| 283 |
elapsed = time.perf_counter() - start
|
| 284 |
total_time += elapsed
|
| 285 |
|
|
|
|
| 289 |
elif hasattr(response, "text"):
|
| 290 |
hypothesis = response.text
|
| 291 |
else:
|
| 292 |
+
raise TypeError(
|
| 293 |
+
"Inference backend returned an unsupported accuracy response "
|
| 294 |
+
f"({type(response).__name__})"
|
| 295 |
+
)
|
| 296 |
hypothesis = normalize_ocr_output(hypothesis)
|
| 297 |
|
| 298 |
# Compute metrics
|
|
|
|
| 308 |
else None
|
| 309 |
)
|
| 310 |
|
| 311 |
+
category = (category_map or {}).get(img_file.name)
|
| 312 |
result = {
|
| 313 |
"file": img_file.name,
|
| 314 |
+
"category": category,
|
| 315 |
"cer": cer,
|
| 316 |
"wer": wer,
|
| 317 |
"digit_cer": digit_cer,
|
|
|
|
| 328 |
}
|
| 329 |
results.append(result)
|
| 330 |
|
| 331 |
+
if save_predictions is not None:
|
| 332 |
+
(save_predictions / f"{img_file.stem}.txt").write_text(
|
| 333 |
+
hypothesis + ("\n" if hypothesis and not hypothesis.endswith("\n") else ""),
|
| 334 |
+
encoding="utf-8",
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
print(f" [{i+1}/{len(evaluation_files)}] {img_file.name}: CER={cer:.4f} "
|
| 338 |
f"DigitCER={digit_cer:.4f} ({elapsed:.1f}s)")
|
| 339 |
|
|
|
|
| 342 |
digit_scores = [r["digit_cer"] for r in results if r["ref_digit_count"] > 0]
|
| 343 |
cjk_scores = [r["cjk_cer"] for r in results if r["ref_cjk_count"] > 0]
|
| 344 |
table_scores = [r["table_score"] for r in results if r["table_score"] is not None]
|
| 345 |
+
generation_settings = {
|
| 346 |
+
"temperature": generation_profile.temperature,
|
| 347 |
+
"top_p": generation_profile.top_p,
|
| 348 |
+
"repetition_penalty": generation_profile.repetition_penalty,
|
| 349 |
+
"no_repeat_ngram_size": generation_profile.no_repeat_ngram_size,
|
| 350 |
+
"ngram_window": 128,
|
| 351 |
+
}
|
| 352 |
+
if backend == "vllm":
|
| 353 |
+
# The official runtime recipe always uses 35, even when an MLX
|
| 354 |
+
# generation profile has an experimental alternate value.
|
| 355 |
+
generation_settings.update({
|
| 356 |
+
"no_repeat_ngram_size": 35,
|
| 357 |
+
"skip_special_tokens": False,
|
| 358 |
+
})
|
| 359 |
+
by_category = aggregate_by_category(results)
|
| 360 |
summary = {
|
| 361 |
"model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
|
| 362 |
+
"served_revision": served_revision,
|
| 363 |
+
"backend": backend,
|
| 364 |
"prompt": prompt,
|
| 365 |
"max_tokens": token_limit,
|
| 366 |
"profile": profile,
|
| 367 |
+
"generation_settings": generation_settings,
|
| 368 |
+
"num_images": len(evaluation_files),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
"num_samples": len(results),
|
| 370 |
"total_time_seconds": total_time,
|
| 371 |
"mean_cer": float(np.mean([r["cer"] for r in results])) if results else 0,
|
|
|
|
| 377 |
"num_table_samples": len(table_scores),
|
| 378 |
"mean_table_score": float(np.mean(table_scores)) if table_scores else None,
|
| 379 |
"mean_repetition_rate": float(np.mean([r["repetition_rate"] for r in results])) if results else 0,
|
| 380 |
+
"by_category": by_category,
|
| 381 |
"per_file": results,
|
| 382 |
}
|
| 383 |
|
|
|
|
| 394 |
help="Override the selected profile's token limit")
|
| 395 |
parser.add_argument("--profile", default="accurate",
|
| 396 |
choices=["accurate", "fast", "long-document", "plain-text", "markdown"])
|
| 397 |
+
parser.add_argument("--backend", default="mlx", choices=["mlx", "vllm"])
|
| 398 |
+
parser.add_argument("--vllm-base-url", default="http://127.0.0.1:8000/v1")
|
| 399 |
+
parser.add_argument("--vllm-api-key", default=os.environ.get("VLLM_API_KEY"))
|
| 400 |
+
parser.add_argument(
|
| 401 |
+
"--served-revision",
|
| 402 |
+
default=None,
|
| 403 |
+
help="Revision pinned when this model was started (required for BF16 release evidence)",
|
| 404 |
+
)
|
| 405 |
+
parser.add_argument(
|
| 406 |
+
"--save-predictions",
|
| 407 |
+
type=Path,
|
| 408 |
+
default=None,
|
| 409 |
+
help="Optional directory to write normalized hypothesis text per image stem",
|
| 410 |
+
)
|
| 411 |
args = parser.parse_args()
|
| 412 |
|
| 413 |
print("=" * 60)
|
|
|
|
| 420 |
prompt=args.prompt,
|
| 421 |
max_tokens=args.max_tokens,
|
| 422 |
profile=args.profile,
|
| 423 |
+
backend=args.backend,
|
| 424 |
+
vllm_base_url=args.vllm_base_url,
|
| 425 |
+
vllm_api_key=args.vllm_api_key,
|
| 426 |
+
served_revision=args.served_revision,
|
| 427 |
+
save_predictions=args.save_predictions,
|
| 428 |
)
|
| 429 |
|
| 430 |
# Save results
|
| 431 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 432 |
+
args.output.write_text(
|
| 433 |
+
json.dumps(results, indent=2, ensure_ascii=False, allow_nan=False),
|
| 434 |
+
encoding="utf-8",
|
| 435 |
+
)
|
| 436 |
|
| 437 |
def metric(value):
|
| 438 |
return "N/A" if value is None else f"{value:.4f}"
|
|
|
|
| 441 |
print(f"Results: CER={results['mean_cer']:.4f} | "
|
| 442 |
f"DigitCER={metric(results['mean_digit_cer'])} | "
|
| 443 |
f"CJK={metric(results['mean_cjk_cer'])}")
|
| 444 |
+
if results.get("by_category"):
|
| 445 |
+
print("By category:")
|
| 446 |
+
for category, stats in results["by_category"].items():
|
| 447 |
+
print(
|
| 448 |
+
f" {category}: n={stats['num_samples']} "
|
| 449 |
+
f"CER={metric(stats['mean_cer'])} "
|
| 450 |
+
f"Digit={metric(stats['mean_digit_cer'])} "
|
| 451 |
+
f"CJK={metric(stats['mean_cjk_cer'])}"
|
| 452 |
+
)
|
| 453 |
print(f"Saved to: {args.output}")
|
| 454 |
|
| 455 |
|
benchmarks/run_performance.py
CHANGED
|
@@ -12,10 +12,11 @@ from __future__ import annotations
|
|
| 12 |
import argparse
|
| 13 |
import importlib.metadata
|
| 14 |
import json
|
| 15 |
-
import
|
| 16 |
import platform
|
| 17 |
import sys
|
| 18 |
import time
|
|
|
|
| 19 |
from pathlib import Path
|
| 20 |
|
| 21 |
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
@@ -83,6 +84,7 @@ def run_performance_benchmark(
|
|
| 83 |
max_tokens: int = 4096,
|
| 84 |
num_warmup: int = 1,
|
| 85 |
num_runs: int = 3,
|
|
|
|
| 86 |
) -> dict:
|
| 87 |
"""Run performance benchmark measuring TPS, memory, and latency."""
|
| 88 |
if not Path(image_path).is_file():
|
|
@@ -95,6 +97,10 @@ def run_performance_benchmark(
|
|
| 95 |
raise ValueError("num_warmup must be a non-negative integer")
|
| 96 |
if not isinstance(num_runs, int) or isinstance(num_runs, bool) or num_runs < 1:
|
| 97 |
raise ValueError("num_runs must be a positive integer")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
from mlx_vlm import load, generate
|
| 100 |
|
|
@@ -105,7 +111,7 @@ def run_performance_benchmark(
|
|
| 105 |
|
| 106 |
# Measure model load time
|
| 107 |
load_start = time.perf_counter()
|
| 108 |
-
model, processor = load(model_path)
|
| 109 |
load_time = time.perf_counter() - load_start
|
| 110 |
mem_after_load = get_memory_usage_mb()
|
| 111 |
|
|
@@ -127,6 +133,8 @@ def run_performance_benchmark(
|
|
| 127 |
|
| 128 |
for i in range(num_runs):
|
| 129 |
active_memory_before_mb = mx.get_active_memory() / 1e6
|
|
|
|
|
|
|
| 130 |
mx.reset_peak_memory()
|
| 131 |
start = time.perf_counter()
|
| 132 |
|
|
@@ -140,7 +148,11 @@ def run_performance_benchmark(
|
|
| 140 |
)
|
| 141 |
|
| 142 |
elapsed = time.perf_counter() - start
|
|
|
|
|
|
|
| 143 |
process_peak_mb = get_memory_usage_mb()
|
|
|
|
|
|
|
| 144 |
|
| 145 |
# Extract token count
|
| 146 |
if isinstance(response, str):
|
|
@@ -179,24 +191,55 @@ def run_performance_benchmark(
|
|
| 179 |
text = str(response)
|
| 180 |
tokens = len(text.split())
|
| 181 |
token_count_source = "whitespace estimate"
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
mlx_peak_mb = (
|
| 196 |
reported_peak_gb * 1000
|
| 197 |
if reported_peak_gb > 0
|
| 198 |
else mx.get_peak_memory() / 1e6
|
| 199 |
)
|
|
|
|
|
|
|
| 200 |
run_result = {
|
| 201 |
"run": i + 1,
|
| 202 |
"elapsed_seconds": elapsed,
|
|
@@ -225,6 +268,7 @@ def run_performance_benchmark(
|
|
| 225 |
|
| 226 |
results = {
|
| 227 |
"model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
|
|
|
|
| 228 |
"image_path": Path(image_path).name,
|
| 229 |
"prompt": prompt,
|
| 230 |
"max_tokens": max_tokens,
|
|
@@ -252,6 +296,11 @@ def main():
|
|
| 252 |
parser.add_argument("--max-tokens", type=int, default=4096)
|
| 253 |
parser.add_argument("--warmup", type=int, default=1)
|
| 254 |
parser.add_argument("--runs", type=int, default=3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
args = parser.parse_args()
|
| 256 |
|
| 257 |
print("=" * 60)
|
|
@@ -265,6 +314,7 @@ def main():
|
|
| 265 |
max_tokens=args.max_tokens,
|
| 266 |
num_warmup=args.warmup,
|
| 267 |
num_runs=args.runs,
|
|
|
|
| 268 |
)
|
| 269 |
|
| 270 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 12 |
import argparse
|
| 13 |
import importlib.metadata
|
| 14 |
import json
|
| 15 |
+
import math
|
| 16 |
import platform
|
| 17 |
import sys
|
| 18 |
import time
|
| 19 |
+
from numbers import Integral, Real
|
| 20 |
from pathlib import Path
|
| 21 |
|
| 22 |
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
| 84 |
max_tokens: int = 4096,
|
| 85 |
num_warmup: int = 1,
|
| 86 |
num_runs: int = 3,
|
| 87 |
+
served_revision: str | None = None,
|
| 88 |
) -> dict:
|
| 89 |
"""Run performance benchmark measuring TPS, memory, and latency."""
|
| 90 |
if not Path(image_path).is_file():
|
|
|
|
| 97 |
raise ValueError("num_warmup must be a non-negative integer")
|
| 98 |
if not isinstance(num_runs, int) or isinstance(num_runs, bool) or num_runs < 1:
|
| 99 |
raise ValueError("num_runs must be a positive integer")
|
| 100 |
+
if served_revision is not None and (
|
| 101 |
+
not isinstance(served_revision, str) or not served_revision.strip()
|
| 102 |
+
):
|
| 103 |
+
raise ValueError("served_revision must be a non-empty string or None")
|
| 104 |
|
| 105 |
from mlx_vlm import load, generate
|
| 106 |
|
|
|
|
| 111 |
|
| 112 |
# Measure model load time
|
| 113 |
load_start = time.perf_counter()
|
| 114 |
+
model, processor = load(model_path, revision=served_revision)
|
| 115 |
load_time = time.perf_counter() - load_start
|
| 116 |
mem_after_load = get_memory_usage_mb()
|
| 117 |
|
|
|
|
| 133 |
|
| 134 |
for i in range(num_runs):
|
| 135 |
active_memory_before_mb = mx.get_active_memory() / 1e6
|
| 136 |
+
if not math.isfinite(active_memory_before_mb) or active_memory_before_mb < 0:
|
| 137 |
+
raise ValueError("MLX returned invalid active memory")
|
| 138 |
mx.reset_peak_memory()
|
| 139 |
start = time.perf_counter()
|
| 140 |
|
|
|
|
| 148 |
)
|
| 149 |
|
| 150 |
elapsed = time.perf_counter() - start
|
| 151 |
+
if not math.isfinite(elapsed) or elapsed <= 0:
|
| 152 |
+
raise ValueError("Performance timer returned an invalid elapsed time")
|
| 153 |
process_peak_mb = get_memory_usage_mb()
|
| 154 |
+
if not math.isfinite(process_peak_mb) or process_peak_mb < 0:
|
| 155 |
+
raise ValueError("Process peak memory is invalid")
|
| 156 |
|
| 157 |
# Extract token count
|
| 158 |
if isinstance(response, str):
|
|
|
|
| 191 |
text = str(response)
|
| 192 |
tokens = len(text.split())
|
| 193 |
token_count_source = "whitespace estimate"
|
| 194 |
+
if (
|
| 195 |
+
not isinstance(tokens, Integral)
|
| 196 |
+
or isinstance(tokens, bool)
|
| 197 |
+
or not 0 < tokens <= max_tokens
|
| 198 |
+
):
|
| 199 |
+
raise ValueError("mlx-vlm returned an invalid generation token count")
|
| 200 |
+
tokens = int(tokens)
|
| 201 |
|
| 202 |
# Prefer mlx-vlm's decode-only rate; the fallback is end-to-end and is
|
| 203 |
# explicitly identified in each run.
|
| 204 |
+
reported_tps = (
|
| 205 |
+
response.get("generation_tps", 0.0)
|
| 206 |
+
if isinstance(response, dict)
|
| 207 |
+
else getattr(response, "generation_tps", 0.0)
|
| 208 |
+
)
|
| 209 |
if reported_tps:
|
| 210 |
+
if (
|
| 211 |
+
not isinstance(reported_tps, Real)
|
| 212 |
+
or isinstance(reported_tps, bool)
|
| 213 |
+
or not math.isfinite(float(reported_tps))
|
| 214 |
+
or reported_tps <= 0
|
| 215 |
+
):
|
| 216 |
+
raise ValueError("mlx-vlm returned invalid generation throughput")
|
| 217 |
tps = float(reported_tps)
|
| 218 |
tps_source = "mlx-vlm generation_tps"
|
| 219 |
else:
|
| 220 |
tps = tokens / elapsed if elapsed > 0 and tokens > 0 else 0
|
| 221 |
tps_source = "end-to-end estimate"
|
| 222 |
|
| 223 |
+
raw_peak_memory = (
|
| 224 |
+
response.get("peak_memory", 0.0)
|
| 225 |
+
if isinstance(response, dict)
|
| 226 |
+
else getattr(response, "peak_memory", 0.0)
|
| 227 |
+
)
|
| 228 |
+
if (
|
| 229 |
+
not isinstance(raw_peak_memory, Real)
|
| 230 |
+
or isinstance(raw_peak_memory, bool)
|
| 231 |
+
or not math.isfinite(float(raw_peak_memory))
|
| 232 |
+
or raw_peak_memory < 0
|
| 233 |
+
):
|
| 234 |
+
raise ValueError("mlx-vlm returned invalid peak memory")
|
| 235 |
+
reported_peak_gb = float(raw_peak_memory)
|
| 236 |
mlx_peak_mb = (
|
| 237 |
reported_peak_gb * 1000
|
| 238 |
if reported_peak_gb > 0
|
| 239 |
else mx.get_peak_memory() / 1e6
|
| 240 |
)
|
| 241 |
+
if not math.isfinite(mlx_peak_mb) or mlx_peak_mb < 0:
|
| 242 |
+
raise ValueError("MLX returned invalid peak memory")
|
| 243 |
run_result = {
|
| 244 |
"run": i + 1,
|
| 245 |
"elapsed_seconds": elapsed,
|
|
|
|
| 268 |
|
| 269 |
results = {
|
| 270 |
"model_path": Path(model_path).name if Path(model_path).is_dir() else model_path,
|
| 271 |
+
"served_revision": served_revision,
|
| 272 |
"image_path": Path(image_path).name,
|
| 273 |
"prompt": prompt,
|
| 274 |
"max_tokens": max_tokens,
|
|
|
|
| 296 |
parser.add_argument("--max-tokens", type=int, default=4096)
|
| 297 |
parser.add_argument("--warmup", type=int, default=1)
|
| 298 |
parser.add_argument("--runs", type=int, default=3)
|
| 299 |
+
parser.add_argument(
|
| 300 |
+
"--served-revision",
|
| 301 |
+
default=None,
|
| 302 |
+
help="Immutable model revision used for this benchmark",
|
| 303 |
+
)
|
| 304 |
args = parser.parse_args()
|
| 305 |
|
| 306 |
print("=" * 60)
|
|
|
|
| 314 |
max_tokens=args.max_tokens,
|
| 315 |
num_warmup=args.warmup,
|
| 316 |
num_runs=args.runs,
|
| 317 |
+
served_revision=args.served_revision,
|
| 318 |
)
|
| 319 |
|
| 320 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
config.json
CHANGED
|
@@ -666,11 +666,6 @@
|
|
| 666 |
"group_size": 32,
|
| 667 |
"bits": 8,
|
| 668 |
"mode": "affine"
|
| 669 |
-
},
|
| 670 |
-
"projector.layers": {
|
| 671 |
-
"group_size": 32,
|
| 672 |
-
"bits": 8,
|
| 673 |
-
"mode": "mxfp8"
|
| 674 |
}
|
| 675 |
},
|
| 676 |
"quantization_config": {
|
|
@@ -1271,11 +1266,6 @@
|
|
| 1271 |
"group_size": 32,
|
| 1272 |
"bits": 8,
|
| 1273 |
"mode": "affine"
|
| 1274 |
-
},
|
| 1275 |
-
"projector.layers": {
|
| 1276 |
-
"group_size": 32,
|
| 1277 |
-
"bits": 8,
|
| 1278 |
-
"mode": "mxfp8"
|
| 1279 |
}
|
| 1280 |
},
|
| 1281 |
"rm_head": false,
|
|
|
|
| 666 |
"group_size": 32,
|
| 667 |
"bits": 8,
|
| 668 |
"mode": "affine"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
}
|
| 670 |
},
|
| 671 |
"quantization_config": {
|
|
|
|
| 1266 |
"group_size": 32,
|
| 1267 |
"bits": 8,
|
| 1268 |
"mode": "affine"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1269 |
}
|
| 1270 |
},
|
| 1271 |
"rm_head": false,
|
docs/PRD.md
CHANGED
|
@@ -62,6 +62,8 @@ performance, provenance, and R-SWA checks pass.
|
|
| 62 |
|
| 63 |
- Validate image/ground-truth pairs before allocating the model.
|
| 64 |
- Record dataset manifest digest and category counts.
|
|
|
|
|
|
|
| 65 |
- Release evaluation must include text, digits, tables, and CJK content.
|
| 66 |
- Synthetic-only evaluation may be used for development, but not labelled as a
|
| 67 |
public accuracy benchmark.
|
|
|
|
| 62 |
|
| 63 |
- Validate image/ground-truth pairs before allocating the model.
|
| 64 |
- Record dataset manifest digest and category counts.
|
| 65 |
+
- Use disjoint, content-addressed datasets for sensitivity/calibration and the
|
| 66 |
+
final release comparison; shared image hashes block publication.
|
| 67 |
- Release evaluation must include text, digits, tables, and CJK content.
|
| 68 |
- Synthetic-only evaluation may be used for development, but not labelled as a
|
| 69 |
public accuracy benchmark.
|
docs/TECHNICAL_SPEC.md
CHANGED
|
@@ -113,6 +113,14 @@ artifacts/<run_id>/
|
|
| 113 |
provenance.json
|
| 114 |
sensitivity_results.json
|
| 115 |
calibration_results.json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
generated_precision_map.json
|
| 117 |
bf16_accuracy.json
|
| 118 |
reference_accuracy.json
|
|
@@ -139,7 +147,11 @@ artifacts without loading a model. Every gate records:
|
|
| 139 |
```
|
| 140 |
|
| 141 |
`release_approved` is true only when every required gate passes. Missing or
|
| 142 |
-
inconclusive values fail the gate.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
### 2.7 Publisher
|
| 145 |
|
|
@@ -163,8 +175,11 @@ Primary orchestration command:
|
|
| 163 |
```bash
|
| 164 |
python quantization/run_pipeline.py \
|
| 165 |
--model-path reference/Unlimited-OCR \
|
|
|
|
| 166 |
--reference-model sahilchachra/unlimited-ocr-mxfp8-mlx \
|
| 167 |
-
--
|
|
|
|
|
|
|
| 168 |
--output-dir models/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
|
| 169 |
--artifacts-dir artifacts/ocr-aware-v1 \
|
| 170 |
--image test_data/test_invoice.png
|
|
@@ -184,20 +199,25 @@ Useful modes:
|
|
| 184 |
|
| 185 |
- `--step preflight`
|
| 186 |
- `--step sensitivity`
|
|
|
|
| 187 |
- `--step precision-map`
|
| 188 |
- `--step convert`
|
| 189 |
- `--step validate`
|
| 190 |
- `--step gate`
|
| 191 |
- `--step publish`
|
| 192 |
-
- `--step all`
|
| 193 |
- `--dry-run`
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
## 4. Provenance schema
|
| 196 |
|
| 197 |
`provenance.json` includes:
|
| 198 |
|
| 199 |
- UTC run ID and timestamps;
|
| 200 |
-
- source and reference identifiers
|
| 201 |
- file SHA-256 and byte sizes;
|
| 202 |
- dataset digest and category counts;
|
| 203 |
- Python, macOS, MLX, mlx-vlm, and huggingface-hub versions;
|
|
@@ -220,6 +240,10 @@ Published `processor_config.json` must use:
|
|
| 220 |
- `processor_class: UnlimitedOCRHFProcessor`;
|
| 221 |
- `sft_format: unlimitedocr`.
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
## 6. Error handling
|
| 224 |
|
| 225 |
- Never delete or overwrite an existing output model directory.
|
|
@@ -258,3 +282,45 @@ After publication:
|
|
| 258 |
3. Load the exact remote revision with `mlx-vlm`.
|
| 259 |
4. Run deterministic OCR on the smoke image.
|
| 260 |
5. Record the final revision in the local release manifest and handoff report.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
provenance.json
|
| 114 |
sensitivity_results.json
|
| 115 |
calibration_results.json
|
| 116 |
+
calibration_baseline_accuracy.json
|
| 117 |
+
calibration_reference_performance.json
|
| 118 |
+
calibration_bfloat16_accuracy.json
|
| 119 |
+
calibration_bfloat16_performance.json
|
| 120 |
+
calibration_mxfp8_accuracy.json
|
| 121 |
+
calibration_mxfp8_performance.json
|
| 122 |
+
calibration_affine8_accuracy.json
|
| 123 |
+
calibration_affine8_performance.json
|
| 124 |
generated_precision_map.json
|
| 125 |
bf16_accuracy.json
|
| 126 |
reference_accuracy.json
|
|
|
|
| 147 |
```
|
| 148 |
|
| 149 |
`release_approved` is true only when every required gate passes. Missing or
|
| 150 |
+
inconclusive values fail the gate. The calibration decision is not trusted as
|
| 151 |
+
an assertion: the gate verifies SHA-256 records for all eight raw calibration
|
| 152 |
+
benchmark files, recomputes their aggregates and quality/throughput deltas, and
|
| 153 |
+
reselects the fastest precision that passes the fixed limits. It also rejects a
|
| 154 |
+
release when calibration and held-out evaluation reuse any image hash.
|
| 155 |
|
| 156 |
### 2.7 Publisher
|
| 157 |
|
|
|
|
| 175 |
```bash
|
| 176 |
python quantization/run_pipeline.py \
|
| 177 |
--model-path reference/Unlimited-OCR \
|
| 178 |
+
--source-revision SOURCE_COMMIT_SHA \
|
| 179 |
--reference-model sahilchachra/unlimited-ocr-mxfp8-mlx \
|
| 180 |
+
--reference-revision REFERENCE_COMMIT_SHA \
|
| 181 |
+
--calibration-dir datasets/selection \
|
| 182 |
+
--eval-dir datasets/held-out-release \
|
| 183 |
--output-dir models/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
|
| 184 |
--artifacts-dir artifacts/ocr-aware-v1 \
|
| 185 |
--image test_data/test_invoice.png
|
|
|
|
| 199 |
|
| 200 |
- `--step preflight`
|
| 201 |
- `--step sensitivity`
|
| 202 |
+
- `--step calibrate` — interim map, three LM-head converts, select affine/mxfp8/bf16 head
|
| 203 |
- `--step precision-map`
|
| 204 |
- `--step convert`
|
| 205 |
- `--step validate`
|
| 206 |
- `--step gate`
|
| 207 |
- `--step publish`
|
| 208 |
+
- `--step all` (runs the full order above)
|
| 209 |
- `--dry-run`
|
| 210 |
|
| 211 |
+
`calibrate` is required before `precision-map` for schema-3 releases: it writes
|
| 212 |
+
`calibration_results.json` plus the eight raw `calibration_*_{accuracy,performance}.json`
|
| 213 |
+
evidence files on the **calibration** dataset only.
|
| 214 |
+
|
| 215 |
## 4. Provenance schema
|
| 216 |
|
| 217 |
`provenance.json` includes:
|
| 218 |
|
| 219 |
- UTC run ID and timestamps;
|
| 220 |
+
- source and reference identifiers plus immutable 40-character commit SHAs;
|
| 221 |
- file SHA-256 and byte sizes;
|
| 222 |
- dataset digest and category counts;
|
| 223 |
- Python, macOS, MLX, mlx-vlm, and huggingface-hub versions;
|
|
|
|
| 240 |
- `processor_class: UnlimitedOCRHFProcessor`;
|
| 241 |
- `sft_format: unlimitedocr`.
|
| 242 |
|
| 243 |
+
If `tokenizer_config.json` is present, it must use the same
|
| 244 |
+
`processor_class: UnlimitedOCRHFProcessor` (convert normalization rewrites both
|
| 245 |
+
files so mlx-vlm and Hub-style loaders do not disagree).
|
| 246 |
+
|
| 247 |
## 6. Error handling
|
| 248 |
|
| 249 |
- Never delete or overwrite an existing output model directory.
|
|
|
|
| 282 |
3. Load the exact remote revision with `mlx-vlm`.
|
| 283 |
4. Run deterministic OCR on the smoke image.
|
| 284 |
5. Record the final revision in the local release manifest and handoff report.
|
| 285 |
+
|
| 286 |
+
## 9. Schema-3 re-release checklist (MLX Hub)
|
| 287 |
+
|
| 288 |
+
The publisher requires `MLX_RELEASE_SCHEMA_VERSION = 3`. A legacy
|
| 289 |
+
`artifacts/ocr-aware-v1/release_manifest.json` with `schema_version: 1` cannot
|
| 290 |
+
be re-uploaded without a full re-gate.
|
| 291 |
+
|
| 292 |
+
1. Build **disjoint** calibration and evaluation trees (no shared FUNSD IDs):
|
| 293 |
+
|
| 294 |
+
```bash
|
| 295 |
+
python eval_dataset/build_release_set.py --clean \
|
| 296 |
+
--output-root eval_dataset/calibration \
|
| 297 |
+
--funsd-ids 0,1 --generator-seed 20260720 \
|
| 298 |
+
--funsd-revision <FUNSD_SHA> --latin-font <PATH> --cjk-font <PATH>
|
| 299 |
+
|
| 300 |
+
python eval_dataset/build_release_set.py --clean \
|
| 301 |
+
--output-root eval_dataset/evaluation \
|
| 302 |
+
--funsd-ids 2,3,4,5 --generator-seed 20260721 \
|
| 303 |
+
--funsd-revision <FUNSD_SHA> --latin-font <PATH> --cjk-font <PATH>
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
2. Run the OCR-aware pipeline / release gate with
|
| 307 |
+
`--calibration-dir eval_dataset/calibration` and
|
| 308 |
+
`--eval-dir eval_dataset/evaluation` (schema 3, all 23 gates).
|
| 309 |
+
3. Confirm convert normalization rewrote both processor and tokenizer
|
| 310 |
+
`processor_class` fields to `UnlimitedOCRHFProcessor`.
|
| 311 |
+
4. Dry-run then publish:
|
| 312 |
+
|
| 313 |
+
```bash
|
| 314 |
+
python scripts/publish_optimized_model.py \
|
| 315 |
+
--model-dir models/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8 \
|
| 316 |
+
--manifest artifacts/<run>/release_manifest.json \
|
| 317 |
+
--artifacts-dir artifacts/<run> \
|
| 318 |
+
--dry-run
|
| 319 |
+
# then without --dry-run when approved
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
5. Post-publish smoke against the remote revision; keep evidence under
|
| 323 |
+
`artifacts/`.
|
| 324 |
+
|
| 325 |
+
Model-card claims must stay local-measurement only until a public protocol
|
| 326 |
+
(OmniDocBench / olmOCR-Bench) is run on the candidate.
|
docs/adr/0002-calibrated-affine8-lm-head.md
CHANGED
|
@@ -4,6 +4,11 @@ Status: Accepted
|
|
| 4 |
Date: 2026-07-20
|
| 5 |
Amends: ADR-0001 for `language_model.lm_head` only
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## Context
|
| 8 |
|
| 9 |
The group sensitivity run showed that the vision encoder must remain BF16, while
|
|
@@ -15,7 +20,7 @@ group sensitivity could not predict:
|
|
| 15 |
|---|---:|---:|---:|---|
|
| 16 |
| BF16 | 0.231904 | 0.082224 | 271.1 | throughput failed |
|
| 17 |
| MXFP8 | 0.260170 | 0.076961 | 307.4 | CER failed |
|
| 18 |
-
| affine INT8 | 0.237384 | 0.085733 | 301.2 |
|
| 19 |
|
| 20 |
The common BF16 baseline CER was 0.239188 and digit CER was 0.087340. The
|
| 21 |
Sahil-reference throughput was 312.7 tok/s. The selection procedure used the
|
|
@@ -54,7 +59,8 @@ fail-closed release design.
|
|
| 54 |
|
| 55 |
- The checkpoint remains predominantly MXFP8 but is accurately described as a
|
| 56 |
mixed MXFP8/affine8 model.
|
| 57 |
-
- The
|
| 58 |
-
while mean CER and digit CER
|
|
|
|
| 59 |
- Consumers must use a recent MLX/`mlx-vlm` version that supports per-module
|
| 60 |
affine and MXFP8 quantization metadata.
|
|
|
|
| 4 |
Date: 2026-07-20
|
| 5 |
Amends: ADR-0001 for `language_model.lm_head` only
|
| 6 |
|
| 7 |
+
Evidence status: this records a historical candidate-construction decision,
|
| 8 |
+
not current release approval. Its calibration and evaluation reused the same
|
| 9 |
+
12 samples. The schema-3 gate now requires disjoint calibration and held-out
|
| 10 |
+
evaluation data before this decision can approve publication.
|
| 11 |
+
|
| 12 |
## Context
|
| 13 |
|
| 14 |
The group sensitivity run showed that the vision encoder must remain BF16, while
|
|
|
|
| 20 |
|---|---:|---:|---:|---|
|
| 21 |
| BF16 | 0.231904 | 0.082224 | 271.1 | throughput failed |
|
| 22 |
| MXFP8 | 0.260170 | 0.076961 | 307.4 | CER failed |
|
| 23 |
+
| affine INT8 | 0.237384 | 0.085733 | 301.2 | development calibration passed |
|
| 24 |
|
| 25 |
The common BF16 baseline CER was 0.239188 and digit CER was 0.087340. The
|
| 26 |
Sahil-reference throughput was 312.7 tok/s. The selection procedure used the
|
|
|
|
| 59 |
|
| 60 |
- The checkpoint remains predominantly MXFP8 but is accurately described as a
|
| 61 |
mixed MXFP8/affine8 model.
|
| 62 |
+
- The historical same-dataset rerun reached 297.2 tok/s, 95.0% of the Sahil
|
| 63 |
+
reference, while mean CER and digit CER were slightly better than BF16. This
|
| 64 |
+
does not replace a disjoint held-out release evaluation.
|
| 65 |
- Consumers must use a recent MLX/`mlx-vlm` version that supports per-module
|
| 66 |
affine and MXFP8 quantization metadata.
|
examples/single_image.py
CHANGED
|
@@ -14,7 +14,7 @@ print(result)
|
|
| 14 |
|
| 15 |
# --- Markdown output ---
|
| 16 |
result = pipeline.run("your_document.jpg", format="markdown", output_path="output.md")
|
| 17 |
-
print(
|
| 18 |
|
| 19 |
# --- With bounding boxes (grounding mode) ---
|
| 20 |
result = pipeline.run("your_document.jpg", format="json", grounding=True)
|
|
|
|
| 14 |
|
| 15 |
# --- Markdown output ---
|
| 16 |
result = pipeline.run("your_document.jpg", format="markdown", output_path="output.md")
|
| 17 |
+
print("Saved to output.md")
|
| 18 |
|
| 19 |
# --- With bounding boxes (grounding mode) ---
|
| 20 |
result = pipeline.run("your_document.jpg", format="json", grounding=True)
|
model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d9fe11fc8d2e333000be6a0e288681231efa69282c844f7775ca40f52db02f0c
|
| 3 |
+
size 3845699993
|
model.safetensors.index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"metadata": {
|
| 3 |
-
"total_size":
|
| 4 |
},
|
| 5 |
"weight_map": {
|
| 6 |
"image_newline": "model.safetensors",
|
|
@@ -280,7 +280,6 @@
|
|
| 280 |
"language_model.model.layers.9.self_attn.v_proj.weight": "model.safetensors",
|
| 281 |
"language_model.model.norm.weight": "model.safetensors",
|
| 282 |
"projector.layers.bias": "model.safetensors",
|
| 283 |
-
"projector.layers.scales": "model.safetensors",
|
| 284 |
"projector.layers.weight": "model.safetensors",
|
| 285 |
"sam_model.blocks.0.attn.proj.bias": "model.safetensors",
|
| 286 |
"sam_model.blocks.0.attn.proj.weight": "model.safetensors",
|
|
|
|
| 1 |
{
|
| 2 |
"metadata": {
|
| 3 |
+
"total_size": 3845606400
|
| 4 |
},
|
| 5 |
"weight_map": {
|
| 6 |
"image_newline": "model.safetensors",
|
|
|
|
| 280 |
"language_model.model.layers.9.self_attn.v_proj.weight": "model.safetensors",
|
| 281 |
"language_model.model.norm.weight": "model.safetensors",
|
| 282 |
"projector.layers.bias": "model.safetensors",
|
|
|
|
| 283 |
"projector.layers.weight": "model.safetensors",
|
| 284 |
"sam_model.blocks.0.attn.proj.bias": "model.safetensors",
|
| 285 |
"sam_model.blocks.0.attn.proj.weight": "model.safetensors",
|
precision_map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"vision_model": "bfloat16",
|
| 3 |
-
"projector": "
|
| 4 |
"language_model.model.embed_tokens": "mxfp8",
|
| 5 |
"language_model.model.norm": "bfloat16",
|
| 6 |
"language_model.lm_head": "affine8",
|
|
@@ -32,16 +32,16 @@
|
|
| 32 |
"vision_encoder": {
|
| 33 |
"precision": "bfloat16",
|
| 34 |
"reasons": [
|
| 35 |
-
"cer_delta=2.
|
| 36 |
-
"digit_cer_delta=1.
|
| 37 |
"table_score_degradation=1.000000",
|
| 38 |
"protected OCR-critical group"
|
| 39 |
]
|
| 40 |
},
|
| 41 |
"vision_projector": {
|
| 42 |
-
"precision": "
|
| 43 |
"reasons": [
|
| 44 |
-
"all measured quality deltas
|
| 45 |
]
|
| 46 |
},
|
| 47 |
"token_embeddings": {
|
|
|
|
| 1 |
{
|
| 2 |
"vision_model": "bfloat16",
|
| 3 |
+
"projector": "bfloat16",
|
| 4 |
"language_model.model.embed_tokens": "mxfp8",
|
| 5 |
"language_model.model.norm": "bfloat16",
|
| 6 |
"language_model.lm_head": "affine8",
|
|
|
|
| 32 |
"vision_encoder": {
|
| 33 |
"precision": "bfloat16",
|
| 34 |
"reasons": [
|
| 35 |
+
"cer_delta=2.272233",
|
| 36 |
+
"digit_cer_delta=1.696454",
|
| 37 |
"table_score_degradation=1.000000",
|
| 38 |
"protected OCR-critical group"
|
| 39 |
]
|
| 40 |
},
|
| 41 |
"vision_projector": {
|
| 42 |
+
"precision": "bfloat16",
|
| 43 |
"reasons": [
|
| 44 |
+
"protected unless all measured quality deltas are non-degrading"
|
| 45 |
]
|
| 46 |
},
|
| 47 |
"token_embeddings": {
|
preprocessing_config.json
CHANGED
|
@@ -66,7 +66,7 @@
|
|
| 66 |
"target_size": 1024
|
| 67 |
},
|
| 68 |
"table": {
|
| 69 |
-
"description": "
|
| 70 |
"deskew": true,
|
| 71 |
"clahe": true,
|
| 72 |
"binarize": false,
|
|
@@ -75,6 +75,29 @@
|
|
| 75 |
"grayscale": false,
|
| 76 |
"target_size": 1024,
|
| 77 |
"clahe_clip_limit": 3.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
},
|
| 80 |
"_note": "Preprocessing is always optional. The raw input path remains available for baseline comparison. Applied steps are recorded in output metadata."
|
|
|
|
| 66 |
"target_size": 1024
|
| 67 |
},
|
| 68 |
"table": {
|
| 69 |
+
"description": "Deskew + CLAHE + sharpening for grid lines and table structure",
|
| 70 |
"deskew": true,
|
| 71 |
"clahe": true,
|
| 72 |
"binarize": false,
|
|
|
|
| 75 |
"grayscale": false,
|
| 76 |
"target_size": 1024,
|
| 77 |
"clahe_clip_limit": 3.0
|
| 78 |
+
},
|
| 79 |
+
"adaptive-binarize": {
|
| 80 |
+
"description": "Detect sideways orientation and apply local Sauvola binarization",
|
| 81 |
+
"deskew": true,
|
| 82 |
+
"clahe": false,
|
| 83 |
+
"binarize": "sauvola",
|
| 84 |
+
"gamma_correction": false,
|
| 85 |
+
"sharpen": false,
|
| 86 |
+
"grayscale": false,
|
| 87 |
+
"target_size": 1024
|
| 88 |
+
},
|
| 89 |
+
"photo-document": {
|
| 90 |
+
"description": "Perspective-correct photographed pages, detect sideways orientation, then enhance contrast and lighting",
|
| 91 |
+
"deskew": false,
|
| 92 |
+
"perspective_correction": true,
|
| 93 |
+
"clahe": true,
|
| 94 |
+
"binarize": false,
|
| 95 |
+
"gamma_correction": true,
|
| 96 |
+
"sharpen": false,
|
| 97 |
+
"grayscale": false,
|
| 98 |
+
"target_size": 1024,
|
| 99 |
+
"clahe_clip_limit": 3.0,
|
| 100 |
+
"gamma_value": 1.3
|
| 101 |
}
|
| 102 |
},
|
| 103 |
"_note": "Preprocessing is always optional. The raw input path remains available for baseline comparison. Applied steps are recorded in output metadata."
|
pyproject.toml
CHANGED
|
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "unlimited-ocr"
|
| 7 |
-
version = "0.
|
| 8 |
-
description = "
|
| 9 |
readme = "README.md"
|
| 10 |
license = "MIT"
|
| 11 |
requires-python = ">=3.10"
|
| 12 |
authors = [
|
| 13 |
{ name = "AutomatosX" },
|
| 14 |
]
|
| 15 |
-
keywords = ["ocr", "mlx", "
|
| 16 |
classifiers = [
|
| 17 |
"Development Status :: 4 - Beta",
|
| 18 |
"Intended Audience :: Developers",
|
|
@@ -20,13 +20,10 @@ classifiers = [
|
|
| 20 |
"Programming Language :: Python :: 3.10",
|
| 21 |
"Programming Language :: Python :: 3.11",
|
| 22 |
"Programming Language :: Python :: 3.12",
|
|
|
|
| 23 |
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 24 |
]
|
| 25 |
dependencies = [
|
| 26 |
-
# Unlimited-OCR support first shipped in mlx-vlm 0.6.4. Older releases
|
| 27 |
-
# cannot load the default model used by this package.
|
| 28 |
-
"mlx-vlm>=0.6.4",
|
| 29 |
-
"mlx>=0.31.2",
|
| 30 |
"Pillow>=10.0",
|
| 31 |
"numpy",
|
| 32 |
"pymupdf>=1.24",
|
|
@@ -39,6 +36,18 @@ dependencies = [
|
|
| 39 |
]
|
| 40 |
|
| 41 |
[project.optional-dependencies]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
server = ["fastapi>=0.100", "uvicorn[standard]>=0.20", "python-multipart>=0.0.5"]
|
| 43 |
dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "httpx>=0.24"]
|
| 44 |
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "unlimited-ocr"
|
| 7 |
+
version = "0.3.0"
|
| 8 |
+
description = "Unlimited-OCR inference toolkit for Apple MLX and NVIDIA CUDA/vLLM"
|
| 9 |
readme = "README.md"
|
| 10 |
license = "MIT"
|
| 11 |
requires-python = ">=3.10"
|
| 12 |
authors = [
|
| 13 |
{ name = "AutomatosX" },
|
| 14 |
]
|
| 15 |
+
keywords = ["ocr", "mlx", "cuda", "vllm", "document-parsing", "vision-language-model"]
|
| 16 |
classifiers = [
|
| 17 |
"Development Status :: 4 - Beta",
|
| 18 |
"Intended Audience :: Developers",
|
|
|
|
| 20 |
"Programming Language :: Python :: 3.10",
|
| 21 |
"Programming Language :: Python :: 3.11",
|
| 22 |
"Programming Language :: Python :: 3.12",
|
| 23 |
+
"Environment :: GPU :: NVIDIA CUDA",
|
| 24 |
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 25 |
]
|
| 26 |
dependencies = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
"Pillow>=10.0",
|
| 28 |
"numpy",
|
| 29 |
"pymupdf>=1.24",
|
|
|
|
| 36 |
]
|
| 37 |
|
| 38 |
[project.optional-dependencies]
|
| 39 |
+
mlx = [
|
| 40 |
+
# Unlimited-OCR support first shipped in mlx-vlm 0.6.4. Keeping MLX
|
| 41 |
+
# optional lets the same wheel install on Linux CUDA hosts.
|
| 42 |
+
"mlx-vlm>=0.6.4; platform_system == 'Darwin'",
|
| 43 |
+
"mlx>=0.31.2; platform_system == 'Darwin'",
|
| 44 |
+
]
|
| 45 |
+
vllm-client = ["httpx>=0.27,<1"]
|
| 46 |
+
vllm-server = [
|
| 47 |
+
# Unlimited-OCR became a native architecture in vLLM 0.25.
|
| 48 |
+
"vllm>=0.25.1,<0.26; platform_system == 'Linux' and platform_machine == 'x86_64'",
|
| 49 |
+
"httpx>=0.27,<1",
|
| 50 |
+
]
|
| 51 |
server = ["fastapi>=0.100", "uvicorn[standard]>=0.20", "python-multipart>=0.0.5"]
|
| 52 |
dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "httpx>=0.24"]
|
| 53 |
|
quantization/calibrate_precision.py
CHANGED
|
@@ -5,14 +5,23 @@ from __future__ import annotations
|
|
| 5 |
|
| 6 |
import argparse
|
| 7 |
import json
|
|
|
|
| 8 |
import sys
|
|
|
|
| 9 |
from datetime import datetime, timezone
|
| 10 |
from pathlib import Path
|
| 11 |
|
| 12 |
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 13 |
sys.path.insert(0, str(PROJECT_ROOT))
|
| 14 |
|
| 15 |
-
from quantization.release_gate import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
SUPPORTED_HEAD_PRECISIONS = {"bfloat16", "mxfp8", "affine8"}
|
|
@@ -20,7 +29,13 @@ SUPPORTED_HEAD_PRECISIONS = {"bfloat16", "mxfp8", "affine8"}
|
|
| 20 |
|
| 21 |
def _number(payload: dict, key: str) -> float | None:
|
| 22 |
value = payload.get(key)
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
def select_head_precision(
|
|
@@ -30,15 +45,34 @@ def select_head_precision(
|
|
| 30 |
thresholds: dict | None = None,
|
| 31 |
) -> dict:
|
| 32 |
"""Choose the fastest experiment that passes the existing release limits."""
|
| 33 |
-
limits =
|
| 34 |
bf16_cer = _number(bf16_accuracy, "mean_cer")
|
| 35 |
bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
|
| 36 |
bf16_table = _number(bf16_accuracy, "mean_table_score")
|
| 37 |
reference_tps = _number(reference_performance, "mean_tps")
|
| 38 |
if None in (bf16_cer, bf16_digit, bf16_table, reference_tps) or reference_tps <= 0:
|
| 39 |
raise ValueError("Baseline accuracy and reference throughput must be complete")
|
| 40 |
-
if not experiments:
|
| 41 |
raise ValueError("At least one calibration experiment is required")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
evaluated = []
|
| 44 |
for experiment in experiments:
|
|
@@ -46,8 +80,6 @@ def select_head_precision(
|
|
| 46 |
precision = experiment.get("precision")
|
| 47 |
accuracy = experiment.get("accuracy")
|
| 48 |
performance = experiment.get("performance")
|
| 49 |
-
if not isinstance(label, str) or not label:
|
| 50 |
-
raise ValueError("Each calibration experiment needs a label")
|
| 51 |
if precision not in SUPPORTED_HEAD_PRECISIONS:
|
| 52 |
raise ValueError(f"Unsupported head precision for {label}: {precision}")
|
| 53 |
if not isinstance(accuracy, dict) or not isinstance(performance, dict):
|
|
@@ -57,40 +89,59 @@ def select_head_precision(
|
|
| 57 |
candidate_table = _number(accuracy, "mean_table_score")
|
| 58 |
candidate_tps = _number(performance, "mean_tps")
|
| 59 |
metrics_complete = None not in (
|
| 60 |
-
candidate_cer,
|
|
|
|
|
|
|
|
|
|
| 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
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
checks = {
|
| 69 |
-
"cer": metrics_complete
|
| 70 |
-
|
| 71 |
-
"
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
-
evaluated.append(
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
"
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
| 87 |
|
| 88 |
passing = [experiment for experiment in evaluated if experiment["passed"]]
|
| 89 |
if not passing:
|
| 90 |
-
raise RuntimeError(
|
|
|
|
|
|
|
| 91 |
selected = max(passing, key=lambda experiment: experiment["metrics"]["mean_tps"])
|
| 92 |
return {
|
| 93 |
-
"schema_version":
|
| 94 |
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 95 |
"target_pattern": "language_model.lm_head",
|
| 96 |
"selection_policy": "fastest candidate passing existing quality and throughput limits",
|
|
@@ -110,6 +161,14 @@ def main() -> None:
|
|
| 110 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 111 |
parser.add_argument("--bf16-accuracy", required=True, type=Path)
|
| 112 |
parser.add_argument("--reference-performance", required=True, type=Path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
parser.add_argument(
|
| 114 |
"--experiment",
|
| 115 |
action="append",
|
|
@@ -119,22 +178,91 @@ def main() -> None:
|
|
| 119 |
)
|
| 120 |
parser.add_argument("--output", required=True, type=Path)
|
| 121 |
args = parser.parse_args()
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
result = select_head_precision(
|
| 132 |
-
|
| 133 |
-
|
| 134 |
experiments,
|
| 135 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
print(json.dumps(result["selected"], ensure_ascii=False))
|
| 139 |
|
| 140 |
|
|
|
|
| 5 |
|
| 6 |
import argparse
|
| 7 |
import json
|
| 8 |
+
import math
|
| 9 |
import sys
|
| 10 |
+
import tempfile
|
| 11 |
from datetime import datetime, timezone
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 15 |
sys.path.insert(0, str(PROJECT_ROOT))
|
| 16 |
|
| 17 |
+
from quantization.release_gate import ( # noqa: E402
|
| 18 |
+
CALIBRATION_EVIDENCE_KEYS,
|
| 19 |
+
dataset_manifest,
|
| 20 |
+
load_json_object,
|
| 21 |
+
sha256_file,
|
| 22 |
+
validate_calibration_results,
|
| 23 |
+
validate_release_thresholds,
|
| 24 |
+
)
|
| 25 |
|
| 26 |
|
| 27 |
SUPPORTED_HEAD_PRECISIONS = {"bfloat16", "mxfp8", "affine8"}
|
|
|
|
| 29 |
|
| 30 |
def _number(payload: dict, key: str) -> float | None:
|
| 31 |
value = payload.get(key)
|
| 32 |
+
if (
|
| 33 |
+
not isinstance(value, (int, float))
|
| 34 |
+
or isinstance(value, bool)
|
| 35 |
+
or not math.isfinite(float(value))
|
| 36 |
+
):
|
| 37 |
+
return None
|
| 38 |
+
return float(value)
|
| 39 |
|
| 40 |
|
| 41 |
def select_head_precision(
|
|
|
|
| 45 |
thresholds: dict | None = None,
|
| 46 |
) -> dict:
|
| 47 |
"""Choose the fastest experiment that passes the existing release limits."""
|
| 48 |
+
limits = validate_release_thresholds(thresholds)
|
| 49 |
bf16_cer = _number(bf16_accuracy, "mean_cer")
|
| 50 |
bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
|
| 51 |
bf16_table = _number(bf16_accuracy, "mean_table_score")
|
| 52 |
reference_tps = _number(reference_performance, "mean_tps")
|
| 53 |
if None in (bf16_cer, bf16_digit, bf16_table, reference_tps) or reference_tps <= 0:
|
| 54 |
raise ValueError("Baseline accuracy and reference throughput must be complete")
|
| 55 |
+
if not isinstance(experiments, list) or not experiments:
|
| 56 |
raise ValueError("At least one calibration experiment is required")
|
| 57 |
+
if any(not isinstance(experiment, dict) for experiment in experiments):
|
| 58 |
+
raise ValueError("Each calibration experiment must be an object")
|
| 59 |
+
labels = [experiment.get("label") for experiment in experiments]
|
| 60 |
+
precisions = [experiment.get("precision") for experiment in experiments]
|
| 61 |
+
if any(
|
| 62 |
+
not isinstance(label, str) or not label.strip() or label.strip() != label
|
| 63 |
+
for label in labels
|
| 64 |
+
):
|
| 65 |
+
raise ValueError("Each calibration experiment needs a normalized label")
|
| 66 |
+
if any(not isinstance(precision, str) for precision in precisions):
|
| 67 |
+
raise ValueError("Each calibration experiment needs a precision")
|
| 68 |
+
if len(set(labels)) != len(labels):
|
| 69 |
+
raise ValueError("Calibration experiment labels must be unique")
|
| 70 |
+
if len(set(precisions)) != len(precisions):
|
| 71 |
+
raise ValueError("Calibration experiment precisions must be unique")
|
| 72 |
+
if set(precisions) != SUPPORTED_HEAD_PRECISIONS:
|
| 73 |
+
raise ValueError(
|
| 74 |
+
"Calibration requires exactly one experiment for each supported precision"
|
| 75 |
+
)
|
| 76 |
|
| 77 |
evaluated = []
|
| 78 |
for experiment in experiments:
|
|
|
|
| 80 |
precision = experiment.get("precision")
|
| 81 |
accuracy = experiment.get("accuracy")
|
| 82 |
performance = experiment.get("performance")
|
|
|
|
|
|
|
| 83 |
if precision not in SUPPORTED_HEAD_PRECISIONS:
|
| 84 |
raise ValueError(f"Unsupported head precision for {label}: {precision}")
|
| 85 |
if not isinstance(accuracy, dict) or not isinstance(performance, dict):
|
|
|
|
| 89 |
candidate_table = _number(accuracy, "mean_table_score")
|
| 90 |
candidate_tps = _number(performance, "mean_tps")
|
| 91 |
metrics_complete = None not in (
|
| 92 |
+
candidate_cer,
|
| 93 |
+
candidate_digit,
|
| 94 |
+
candidate_table,
|
| 95 |
+
candidate_tps,
|
| 96 |
)
|
| 97 |
deltas = {
|
| 98 |
"cer_vs_bf16": candidate_cer - bf16_cer if metrics_complete else None,
|
| 99 |
+
"digit_cer_vs_bf16": candidate_digit - bf16_digit
|
| 100 |
+
if metrics_complete
|
| 101 |
+
else None,
|
| 102 |
+
"table_degradation_vs_bf16": bf16_table - candidate_table
|
| 103 |
+
if metrics_complete
|
| 104 |
+
else None,
|
| 105 |
+
"tps_ratio_vs_reference": candidate_tps / reference_tps
|
| 106 |
+
if metrics_complete
|
| 107 |
+
else None,
|
| 108 |
}
|
| 109 |
checks = {
|
| 110 |
+
"cer": metrics_complete
|
| 111 |
+
and deltas["cer_vs_bf16"] <= limits["max_cer_delta_vs_bf16"],
|
| 112 |
+
"digit_cer": metrics_complete
|
| 113 |
+
and deltas["digit_cer_vs_bf16"] <= limits["max_digit_cer_delta_vs_bf16"],
|
| 114 |
+
"table_score": metrics_complete
|
| 115 |
+
and deltas["table_degradation_vs_bf16"]
|
| 116 |
+
<= limits["max_table_score_degradation_vs_bf16"],
|
| 117 |
+
"throughput": metrics_complete
|
| 118 |
+
and deltas["tps_ratio_vs_reference"]
|
| 119 |
+
>= limits["min_tps_ratio_vs_reference"],
|
| 120 |
}
|
| 121 |
+
evaluated.append(
|
| 122 |
+
{
|
| 123 |
+
"label": label,
|
| 124 |
+
"precision": precision,
|
| 125 |
+
"passed": all(checks.values()),
|
| 126 |
+
"checks": checks,
|
| 127 |
+
"metrics": {
|
| 128 |
+
"mean_cer": candidate_cer,
|
| 129 |
+
"mean_digit_cer": candidate_digit,
|
| 130 |
+
"mean_table_score": candidate_table,
|
| 131 |
+
"mean_tps": candidate_tps,
|
| 132 |
+
},
|
| 133 |
+
"deltas": deltas,
|
| 134 |
+
}
|
| 135 |
+
)
|
| 136 |
|
| 137 |
passing = [experiment for experiment in evaluated if experiment["passed"]]
|
| 138 |
if not passing:
|
| 139 |
+
raise RuntimeError(
|
| 140 |
+
"No LM-head calibration experiment passed every release limit"
|
| 141 |
+
)
|
| 142 |
selected = max(passing, key=lambda experiment: experiment["metrics"]["mean_tps"])
|
| 143 |
return {
|
| 144 |
+
"schema_version": 3,
|
| 145 |
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 146 |
"target_pattern": "language_model.lm_head",
|
| 147 |
"selection_policy": "fastest candidate passing existing quality and throughput limits",
|
|
|
|
| 161 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 162 |
parser.add_argument("--bf16-accuracy", required=True, type=Path)
|
| 163 |
parser.add_argument("--reference-performance", required=True, type=Path)
|
| 164 |
+
parser.add_argument(
|
| 165 |
+
"--calibration-dir",
|
| 166 |
+
required=True,
|
| 167 |
+
type=Path,
|
| 168 |
+
help="Selection-only dataset used for every calibration accuracy run",
|
| 169 |
+
)
|
| 170 |
+
parser.add_argument("--source-revision", required=True)
|
| 171 |
+
parser.add_argument("--reference-revision", required=True)
|
| 172 |
parser.add_argument(
|
| 173 |
"--experiment",
|
| 174 |
action="append",
|
|
|
|
| 178 |
)
|
| 179 |
parser.add_argument("--output", required=True, type=Path)
|
| 180 |
args = parser.parse_args()
|
| 181 |
+
bf16_accuracy = load_json_object(args.bf16_accuracy)
|
| 182 |
+
reference_performance = load_json_object(args.reference_performance)
|
| 183 |
+
experiments = []
|
| 184 |
+
calibration_dataset = dataset_manifest(args.calibration_dir)
|
| 185 |
+
evidence = {
|
| 186 |
+
"calibration_baseline_accuracy": bf16_accuracy,
|
| 187 |
+
"calibration_reference_performance": reference_performance,
|
| 188 |
+
}
|
| 189 |
+
evidence_paths: dict[str, Path] = {
|
| 190 |
+
"calibration_baseline_accuracy": args.bf16_accuracy,
|
| 191 |
+
"calibration_reference_performance": args.reference_performance,
|
| 192 |
+
}
|
| 193 |
+
for label, precision, accuracy_path, performance_path in args.experiment:
|
| 194 |
+
if precision not in CALIBRATION_EVIDENCE_KEYS:
|
| 195 |
+
raise ValueError(f"Unsupported calibration precision: {precision}")
|
| 196 |
+
accuracy_key, performance_key = CALIBRATION_EVIDENCE_KEYS[precision]
|
| 197 |
+
accuracy_path = Path(accuracy_path)
|
| 198 |
+
performance_path = Path(performance_path)
|
| 199 |
+
evidence[accuracy_key] = load_json_object(accuracy_path)
|
| 200 |
+
evidence[performance_key] = load_json_object(performance_path)
|
| 201 |
+
evidence_paths[accuracy_key] = accuracy_path
|
| 202 |
+
evidence_paths[performance_key] = performance_path
|
| 203 |
+
experiments.append(
|
| 204 |
+
{
|
| 205 |
+
"label": label,
|
| 206 |
+
"precision": precision,
|
| 207 |
+
"accuracy": evidence[accuracy_key],
|
| 208 |
+
"performance": evidence[performance_key],
|
| 209 |
+
}
|
| 210 |
+
)
|
| 211 |
result = select_head_precision(
|
| 212 |
+
bf16_accuracy,
|
| 213 |
+
reference_performance,
|
| 214 |
experiments,
|
| 215 |
)
|
| 216 |
+
expected_input_keys = {
|
| 217 |
+
"calibration_baseline_accuracy",
|
| 218 |
+
"calibration_reference_performance",
|
| 219 |
+
*(name for pair in CALIBRATION_EVIDENCE_KEYS.values() for name in pair),
|
| 220 |
+
}
|
| 221 |
+
if set(evidence_paths) != expected_input_keys:
|
| 222 |
+
raise ValueError("Calibration requires one unique evidence pair per precision")
|
| 223 |
+
resolved_paths = [path.resolve(strict=True) for path in evidence_paths.values()]
|
| 224 |
+
if len(set(resolved_paths)) != len(resolved_paths):
|
| 225 |
+
raise ValueError("Calibration input artifacts must be distinct files")
|
| 226 |
+
result["input_artifacts"] = {
|
| 227 |
+
name: {
|
| 228 |
+
"filename": path.name,
|
| 229 |
+
"size": path.stat().st_size,
|
| 230 |
+
"sha256": sha256_file(path),
|
| 231 |
+
}
|
| 232 |
+
for name, path in evidence_paths.items()
|
| 233 |
+
}
|
| 234 |
+
result["dataset"] = calibration_dataset
|
| 235 |
+
validate_calibration_results(
|
| 236 |
+
result,
|
| 237 |
+
bf16_accuracy=bf16_accuracy,
|
| 238 |
+
reference_performance=reference_performance,
|
| 239 |
+
calibration_dataset=calibration_dataset,
|
| 240 |
+
source_revision=args.source_revision,
|
| 241 |
+
reference_revision=args.reference_revision,
|
| 242 |
+
evidence=evidence,
|
| 243 |
+
evidence_paths=evidence_paths,
|
| 244 |
+
)
|
| 245 |
+
if args.output.is_symlink() or (args.output.exists() and not args.output.is_file()):
|
| 246 |
+
raise ValueError(f"Output must be a regular file: {args.output}")
|
| 247 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 248 |
+
temporary_path = None
|
| 249 |
+
try:
|
| 250 |
+
with tempfile.NamedTemporaryFile(
|
| 251 |
+
mode="w",
|
| 252 |
+
encoding="utf-8",
|
| 253 |
+
dir=args.output.parent,
|
| 254 |
+
prefix=f".{args.output.name}.",
|
| 255 |
+
suffix=".tmp",
|
| 256 |
+
delete=False,
|
| 257 |
+
) as temporary:
|
| 258 |
+
temporary_path = Path(temporary.name)
|
| 259 |
+
json.dump(result, temporary, indent=2, ensure_ascii=False, allow_nan=False)
|
| 260 |
+
temporary.write("\n")
|
| 261 |
+
temporary_path.replace(args.output)
|
| 262 |
+
temporary_path = None
|
| 263 |
+
finally:
|
| 264 |
+
if temporary_path is not None:
|
| 265 |
+
temporary_path.unlink(missing_ok=True)
|
| 266 |
print(json.dumps(result["selected"], ensure_ascii=False))
|
| 267 |
|
| 268 |
|
quantization/layer_sensitivity.py
CHANGED
|
@@ -29,12 +29,13 @@ PROJECT_ROOT = Path(__file__).parent.parent
|
|
| 29 |
sys.path.insert(0, str(PROJECT_ROOT))
|
| 30 |
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
| 31 |
|
| 32 |
-
from benchmarks.evaluate_cer import compute_cer as _compute_cer
|
| 33 |
-
from benchmarks.evaluate_cer import compute_digit_cer as _compute_digit_cer
|
| 34 |
-
from benchmarks.evaluate_tables import evaluate_tables
|
| 35 |
-
from benchmarks.normalize_output import normalize_ocr_output
|
| 36 |
-
from quantization.mixed_precision_convert import matches_pattern
|
| 37 |
-
from
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
# Quantizable MLX module groups to test independently. Normalization and the
|
|
@@ -239,6 +240,11 @@ def main():
|
|
| 239 |
default=None,
|
| 240 |
help="Public source identifier recorded in results (defaults to repo ID or local name)",
|
| 241 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
parser.add_argument("--eval-dir", required=True, type=Path,
|
| 243 |
help="Evaluation directory with images/ and ground_truth/")
|
| 244 |
parser.add_argument("--output", type=Path, default=Path("sensitivity_results.json"),
|
|
@@ -259,6 +265,7 @@ def main():
|
|
| 259 |
|
| 260 |
# Fail before importing MLX or allocating the model for dataset mistakes.
|
| 261 |
collect_evaluation_files(args.eval_dir, args.prompt, args.max_tokens)
|
|
|
|
| 262 |
|
| 263 |
print("=" * 60)
|
| 264 |
print("Layer Sensitivity Analysis for Unlimited-OCR MLX")
|
|
@@ -270,12 +277,12 @@ def main():
|
|
| 270 |
from mlx_vlm.utils import load_config
|
| 271 |
import mlx.core as mx
|
| 272 |
|
| 273 |
-
base_config = load_config(args.model_path)
|
| 274 |
if base_config.get("quantization") or base_config.get("quantization_config"):
|
| 275 |
raise ValueError(
|
| 276 |
"Layer sensitivity requires an unquantized BF16 source model"
|
| 277 |
)
|
| 278 |
-
model, processor = load(args.model_path)
|
| 279 |
|
| 280 |
# Step 2: Evaluate baseline
|
| 281 |
print("\n[2/3] Evaluating BF16 baseline...")
|
|
@@ -306,7 +313,9 @@ def main():
|
|
| 306 |
"model_path": args.source_id or (
|
| 307 |
Path(args.model_path).name if Path(args.model_path).is_dir() else args.model_path
|
| 308 |
),
|
|
|
|
| 309 |
"eval_dir": args.eval_dir.name,
|
|
|
|
| 310 |
"prompt": args.prompt,
|
| 311 |
"max_tokens": args.max_tokens,
|
| 312 |
"groups_tested": args.groups,
|
|
@@ -318,7 +327,10 @@ def main():
|
|
| 318 |
group_model = None
|
| 319 |
group_processor = None
|
| 320 |
try:
|
| 321 |
-
group_model, group_processor = load(
|
|
|
|
|
|
|
|
|
|
| 322 |
matched_modules = quantize_layer_group(
|
| 323 |
group_model,
|
| 324 |
dict(base_config),
|
|
|
|
| 29 |
sys.path.insert(0, str(PROJECT_ROOT))
|
| 30 |
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
| 31 |
|
| 32 |
+
from benchmarks.evaluate_cer import compute_cer as _compute_cer # noqa: E402
|
| 33 |
+
from benchmarks.evaluate_cer import compute_digit_cer as _compute_digit_cer # noqa: E402
|
| 34 |
+
from benchmarks.evaluate_tables import evaluate_tables # noqa: E402
|
| 35 |
+
from benchmarks.normalize_output import normalize_ocr_output # noqa: E402
|
| 36 |
+
from quantization.mixed_precision_convert import matches_pattern # noqa: E402
|
| 37 |
+
from quantization.release_gate import dataset_manifest # noqa: E402
|
| 38 |
+
from unlimited_ocr.engine import SlidingWindowNoRepeatNGramProcessor # noqa: E402
|
| 39 |
|
| 40 |
|
| 41 |
# Quantizable MLX module groups to test independently. Normalization and the
|
|
|
|
| 240 |
default=None,
|
| 241 |
help="Public source identifier recorded in results (defaults to repo ID or local name)",
|
| 242 |
)
|
| 243 |
+
parser.add_argument(
|
| 244 |
+
"--source-revision",
|
| 245 |
+
default=None,
|
| 246 |
+
help="Immutable source commit used for remote loading and provenance",
|
| 247 |
+
)
|
| 248 |
parser.add_argument("--eval-dir", required=True, type=Path,
|
| 249 |
help="Evaluation directory with images/ and ground_truth/")
|
| 250 |
parser.add_argument("--output", type=Path, default=Path("sensitivity_results.json"),
|
|
|
|
| 265 |
|
| 266 |
# Fail before importing MLX or allocating the model for dataset mistakes.
|
| 267 |
collect_evaluation_files(args.eval_dir, args.prompt, args.max_tokens)
|
| 268 |
+
dataset = dataset_manifest(args.eval_dir)
|
| 269 |
|
| 270 |
print("=" * 60)
|
| 271 |
print("Layer Sensitivity Analysis for Unlimited-OCR MLX")
|
|
|
|
| 277 |
from mlx_vlm.utils import load_config
|
| 278 |
import mlx.core as mx
|
| 279 |
|
| 280 |
+
base_config = load_config(args.model_path, revision=args.source_revision)
|
| 281 |
if base_config.get("quantization") or base_config.get("quantization_config"):
|
| 282 |
raise ValueError(
|
| 283 |
"Layer sensitivity requires an unquantized BF16 source model"
|
| 284 |
)
|
| 285 |
+
model, processor = load(args.model_path, revision=args.source_revision)
|
| 286 |
|
| 287 |
# Step 2: Evaluate baseline
|
| 288 |
print("\n[2/3] Evaluating BF16 baseline...")
|
|
|
|
| 313 |
"model_path": args.source_id or (
|
| 314 |
Path(args.model_path).name if Path(args.model_path).is_dir() else args.model_path
|
| 315 |
),
|
| 316 |
+
"source_revision": args.source_revision,
|
| 317 |
"eval_dir": args.eval_dir.name,
|
| 318 |
+
"dataset": dataset,
|
| 319 |
"prompt": args.prompt,
|
| 320 |
"max_tokens": args.max_tokens,
|
| 321 |
"groups_tested": args.groups,
|
|
|
|
| 327 |
group_model = None
|
| 328 |
group_processor = None
|
| 329 |
try:
|
| 330 |
+
group_model, group_processor = load(
|
| 331 |
+
args.model_path,
|
| 332 |
+
revision=args.source_revision,
|
| 333 |
+
)
|
| 334 |
matched_modules = quantize_layer_group(
|
| 335 |
group_model,
|
| 336 |
dict(base_config),
|
quantization/mixed_precision_convert.py
CHANGED
|
@@ -104,6 +104,28 @@ def normalize_unlimited_ocr_metadata(model_dir: Path) -> None:
|
|
| 104 |
encoding="utf-8",
|
| 105 |
)
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
def load_precision_map(path: Path) -> dict:
|
| 109 |
"""Load the precision map JSON."""
|
|
@@ -178,6 +200,8 @@ def convert_model(
|
|
| 178 |
precision_map: dict,
|
| 179 |
output_dir: Path,
|
| 180 |
verbose: bool = True,
|
|
|
|
|
|
|
| 181 |
) -> Path:
|
| 182 |
"""Apply mixed-precision quantization according to the precision map.
|
| 183 |
|
|
@@ -211,7 +235,7 @@ def convert_model(
|
|
| 211 |
return False
|
| 212 |
|
| 213 |
if verbose:
|
| 214 |
-
print(
|
| 215 |
for pattern, precision in precision_map.items():
|
| 216 |
if not pattern.startswith("_"):
|
| 217 |
print(f" {pattern}: {precision}")
|
|
@@ -224,6 +248,7 @@ def convert_model(
|
|
| 224 |
convert(
|
| 225 |
hf_path=model_path,
|
| 226 |
mlx_path=str(staging_dir),
|
|
|
|
| 227 |
quantize=True,
|
| 228 |
q_group_size=32,
|
| 229 |
q_bits=8,
|
|
@@ -255,7 +280,7 @@ def convert_model(
|
|
| 255 |
normalize_unlimited_ocr_metadata(staging_dir)
|
| 256 |
|
| 257 |
(staging_dir / "precision_map.json").write_text(
|
| 258 |
-
json.dumps(precision_map, indent=2),
|
| 259 |
encoding="utf-8",
|
| 260 |
)
|
| 261 |
(staging_dir / "quantization_summary.json").write_text(
|
|
@@ -266,6 +291,7 @@ def convert_model(
|
|
| 266 |
"source_model": (
|
| 267 |
Path(model_path).name if Path(model_path).is_dir() else model_path
|
| 268 |
),
|
|
|
|
| 269 |
"precision_map_sha256": _json_digest(precision_map),
|
| 270 |
"quantized_module_count": len(set(quantized_modules)),
|
| 271 |
"quantized_precision_counts": {
|
|
@@ -298,6 +324,11 @@ def main():
|
|
| 298 |
)
|
| 299 |
parser.add_argument("--model-path", required=True,
|
| 300 |
help="Source model (baidu/Unlimited-OCR or local path)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
parser.add_argument("--precision-map", type=Path,
|
| 302 |
default=Path(__file__).parent / "precision_map.json",
|
| 303 |
help="Path to precision_map.json")
|
|
@@ -316,7 +347,13 @@ def main():
|
|
| 316 |
print("=" * 60)
|
| 317 |
|
| 318 |
precision_map = load_precision_map(args.precision_map)
|
| 319 |
-
convert_model(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
|
| 322 |
if __name__ == "__main__":
|
|
|
|
| 104 |
encoding="utf-8",
|
| 105 |
)
|
| 106 |
|
| 107 |
+
# mlx-vlm convert may stamp a different processor_class on tokenizer_config.
|
| 108 |
+
# Keep it aligned so Hub/transformers-style loaders and mlx-vlm agree.
|
| 109 |
+
tokenizer_config_path = model_dir / "tokenizer_config.json"
|
| 110 |
+
if tokenizer_config_path.is_file():
|
| 111 |
+
try:
|
| 112 |
+
tokenizer_config = json.loads(
|
| 113 |
+
tokenizer_config_path.read_text(encoding="utf-8")
|
| 114 |
+
)
|
| 115 |
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
"Converted model has invalid tokenizer_config.json"
|
| 118 |
+
) from exc
|
| 119 |
+
if not isinstance(tokenizer_config, dict):
|
| 120 |
+
raise ValueError(
|
| 121 |
+
"Converted tokenizer_config.json must contain a JSON object"
|
| 122 |
+
)
|
| 123 |
+
tokenizer_config["processor_class"] = "UnlimitedOCRHFProcessor"
|
| 124 |
+
tokenizer_config_path.write_text(
|
| 125 |
+
json.dumps(tokenizer_config, indent=2, ensure_ascii=False) + "\n",
|
| 126 |
+
encoding="utf-8",
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
|
| 130 |
def load_precision_map(path: Path) -> dict:
|
| 131 |
"""Load the precision map JSON."""
|
|
|
|
| 200 |
precision_map: dict,
|
| 201 |
output_dir: Path,
|
| 202 |
verbose: bool = True,
|
| 203 |
+
*,
|
| 204 |
+
source_revision: str | None = None,
|
| 205 |
) -> Path:
|
| 206 |
"""Apply mixed-precision quantization according to the precision map.
|
| 207 |
|
|
|
|
| 235 |
return False
|
| 236 |
|
| 237 |
if verbose:
|
| 238 |
+
print("\nPrecision map summary:")
|
| 239 |
for pattern, precision in precision_map.items():
|
| 240 |
if not pattern.startswith("_"):
|
| 241 |
print(f" {pattern}: {precision}")
|
|
|
|
| 248 |
convert(
|
| 249 |
hf_path=model_path,
|
| 250 |
mlx_path=str(staging_dir),
|
| 251 |
+
revision=source_revision,
|
| 252 |
quantize=True,
|
| 253 |
q_group_size=32,
|
| 254 |
q_bits=8,
|
|
|
|
| 280 |
normalize_unlimited_ocr_metadata(staging_dir)
|
| 281 |
|
| 282 |
(staging_dir / "precision_map.json").write_text(
|
| 283 |
+
json.dumps(precision_map, indent=2, ensure_ascii=False) + "\n",
|
| 284 |
encoding="utf-8",
|
| 285 |
)
|
| 286 |
(staging_dir / "quantization_summary.json").write_text(
|
|
|
|
| 291 |
"source_model": (
|
| 292 |
Path(model_path).name if Path(model_path).is_dir() else model_path
|
| 293 |
),
|
| 294 |
+
"source_revision": source_revision,
|
| 295 |
"precision_map_sha256": _json_digest(precision_map),
|
| 296 |
"quantized_module_count": len(set(quantized_modules)),
|
| 297 |
"quantized_precision_counts": {
|
|
|
|
| 324 |
)
|
| 325 |
parser.add_argument("--model-path", required=True,
|
| 326 |
help="Source model (baidu/Unlimited-OCR or local path)")
|
| 327 |
+
parser.add_argument(
|
| 328 |
+
"--source-revision",
|
| 329 |
+
default=None,
|
| 330 |
+
help="Immutable source commit used for remote loading and provenance",
|
| 331 |
+
)
|
| 332 |
parser.add_argument("--precision-map", type=Path,
|
| 333 |
default=Path(__file__).parent / "precision_map.json",
|
| 334 |
help="Path to precision_map.json")
|
|
|
|
| 347 |
print("=" * 60)
|
| 348 |
|
| 349 |
precision_map = load_precision_map(args.precision_map)
|
| 350 |
+
convert_model(
|
| 351 |
+
args.model_path,
|
| 352 |
+
precision_map,
|
| 353 |
+
args.output_dir,
|
| 354 |
+
args.verbose,
|
| 355 |
+
source_revision=args.source_revision,
|
| 356 |
+
)
|
| 357 |
|
| 358 |
|
| 359 |
if __name__ == "__main__":
|
quantization/release_gate.py
CHANGED
|
@@ -7,40 +7,226 @@ import argparse
|
|
| 7 |
import hashlib
|
| 8 |
import importlib.metadata
|
| 9 |
import json
|
|
|
|
| 10 |
import platform
|
|
|
|
| 11 |
from collections import Counter
|
| 12 |
from datetime import datetime, timezone
|
| 13 |
-
from pathlib import Path
|
| 14 |
from typing import Any
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".bmp"}
|
| 18 |
HARD_PROTECTED_GROUPS = {"vision_encoder", "lm_head"}
|
| 19 |
EVIDENCE_PROTECTED_GROUPS = {"vision_projector", "token_embeddings"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
DEFAULT_THRESHOLDS = {
|
| 21 |
-
|
|
|
|
| 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(
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
raise ValueError(f"Invalid JSON file: {path}") from exc
|
| 37 |
if not isinstance(value, dict):
|
| 38 |
raise ValueError(f"JSON file must contain an object: {path}")
|
| 39 |
return value
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def sha256_file(path: str | Path, chunk_size: int = 8 * 1024 * 1024) -> str:
|
| 43 |
"""Return the SHA-256 digest of a file without loading it into memory."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
digest = hashlib.sha256()
|
| 45 |
with Path(path).open("rb") as handle:
|
| 46 |
while chunk := handle.read(chunk_size):
|
|
@@ -48,10 +234,91 @@ def sha256_file(path: str | Path, chunk_size: int = 8 * 1024 * 1024) -> str:
|
|
| 48 |
return digest.hexdigest()
|
| 49 |
|
| 50 |
|
| 51 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
"""Hash Safetensors files and create a stable aggregate digest."""
|
|
|
|
|
|
|
| 53 |
model_dir = Path(model_dir)
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
if not weight_paths:
|
| 56 |
raise FileNotFoundError(f"No Safetensors weights found in: {model_dir}")
|
| 57 |
|
|
@@ -78,37 +345,173 @@ def model_weight_manifest(model_dir: str | Path) -> dict:
|
|
| 78 |
}
|
| 79 |
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
def dataset_manifest(eval_dir: str | Path) -> dict:
|
| 82 |
-
"""Validate and content-hash an OCR image/ground-truth dataset."""
|
| 83 |
eval_dir = Path(eval_dir)
|
| 84 |
images_dir = eval_dir / "images"
|
| 85 |
ground_truth_dir = eval_dir / "ground_truth"
|
| 86 |
manifest_path = eval_dir / "manifest.json"
|
|
|
|
|
|
|
| 87 |
if not images_dir.is_dir() or not ground_truth_dir.is_dir():
|
| 88 |
raise FileNotFoundError("Evaluation dataset requires images/ and ground_truth/")
|
|
|
|
|
|
|
| 89 |
declared = load_json_object(manifest_path)
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
if not image_paths:
|
| 96 |
raise ValueError(f"Evaluation dataset contains no images: {images_dir}")
|
| 97 |
duplicate_stems = sorted(
|
| 98 |
-
stem
|
|
|
|
| 99 |
if count > 1
|
| 100 |
)
|
| 101 |
if duplicate_stems:
|
| 102 |
raise ValueError("Duplicate image stems: " + ", ".join(duplicate_stems))
|
| 103 |
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
content_digest = hashlib.sha256()
|
| 114 |
files = []
|
|
@@ -118,25 +521,43 @@ def dataset_manifest(eval_dir: str | Path) -> dict:
|
|
| 118 |
table_samples = 0
|
| 119 |
for image_path in image_paths:
|
| 120 |
relative_image = image_path.relative_to(eval_dir).as_posix()
|
| 121 |
-
sample = samples_by_image
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
if not ground_truth.strip():
|
| 129 |
raise ValueError(f"Ground truth is empty: {gt_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
category_counts[str(sample
|
| 132 |
-
digit_samples += int(
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
for path in (image_path, gt_path):
|
| 142 |
relative = path.relative_to(eval_dir).as_posix()
|
|
@@ -148,12 +569,6 @@ def dataset_manifest(eval_dir: str | Path) -> dict:
|
|
| 148 |
content_digest.update(b"\n")
|
| 149 |
files.append({"path": relative, "size": size, "sha256": digest})
|
| 150 |
|
| 151 |
-
declared_images = set(samples_by_image)
|
| 152 |
-
actual_images = {path.relative_to(eval_dir).as_posix() for path in image_paths}
|
| 153 |
-
extra = sorted(declared_images - actual_images)
|
| 154 |
-
if extra:
|
| 155 |
-
raise FileNotFoundError("Manifest references missing images: " + ", ".join(extra))
|
| 156 |
-
|
| 157 |
content_digest.update(manifest_path.read_bytes())
|
| 158 |
return {
|
| 159 |
"name": eval_dir.name,
|
|
@@ -168,11 +583,69 @@ def dataset_manifest(eval_dir: str | Path) -> dict:
|
|
| 168 |
}
|
| 169 |
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
def _pattern_is_within(child: str, parent: str) -> bool:
|
| 172 |
"""Return whether a precision rule is the parent itself or a descendant."""
|
| 173 |
return child == parent or child.startswith(parent + ".")
|
| 174 |
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
def generate_precision_map(
|
| 177 |
base_map: dict,
|
| 178 |
sensitivity_results: dict,
|
|
@@ -185,6 +658,13 @@ def generate_precision_map(
|
|
| 185 |
"""Turn group sensitivity measurements into executable top-level rules."""
|
| 186 |
from quantization.layer_sensitivity import LAYER_GROUPS
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
groups = sensitivity_results.get("layer_groups")
|
| 189 |
if not isinstance(groups, dict) or not groups:
|
| 190 |
raise ValueError("Sensitivity results contain no layer_groups")
|
|
@@ -206,30 +686,25 @@ def generate_precision_map(
|
|
| 206 |
("digit_cer_delta", digit_cer_threshold),
|
| 207 |
("table_score_degradation", table_degradation_threshold),
|
| 208 |
):
|
| 209 |
-
value = result.get(key)
|
| 210 |
-
if
|
|
|
|
|
|
|
| 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 |
-
|
| 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(
|
|
|
|
|
|
|
| 233 |
|
| 234 |
retain_bf16 = bool(reasons)
|
| 235 |
if retain_bf16:
|
|
@@ -246,11 +721,14 @@ def generate_precision_map(
|
|
| 246 |
generated[group_pattern] = "mxfp8"
|
| 247 |
decisions[group_name] = {
|
| 248 |
"precision": (
|
| 249 |
-
"bfloat16"
|
| 250 |
-
|
|
|
|
|
|
|
| 251 |
else "base-map"
|
| 252 |
),
|
| 253 |
-
"reasons": reasons
|
|
|
|
| 254 |
["all measured quality deltas were non-degrading"]
|
| 255 |
if group_name in EVIDENCE_PROTECTED_GROUPS
|
| 256 |
else ["within thresholds"]
|
|
@@ -262,10 +740,38 @@ def generate_precision_map(
|
|
| 262 |
overrides = calibration_results.get("precision_overrides")
|
| 263 |
if not isinstance(overrides, dict) or not overrides:
|
| 264 |
raise ValueError("Calibration results contain no precision_overrides")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
allowed_precisions = {"bfloat16", "mxfp8", "affine8"}
|
| 266 |
for pattern, precision in overrides.items():
|
| 267 |
if not isinstance(pattern, str) or precision not in allowed_precisions:
|
| 268 |
-
raise ValueError(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
generated[pattern] = precision
|
| 270 |
for group_name, patterns in LAYER_GROUPS.items():
|
| 271 |
if pattern in patterns:
|
|
@@ -299,30 +805,548 @@ def validate_candidate_metadata(model_dir: str | Path) -> dict:
|
|
| 299 |
model_dir = Path(model_dir)
|
| 300 |
config = load_json_object(model_dir / "config.json")
|
| 301 |
processor = load_json_object(model_dir / "processor_config.json")
|
|
|
|
|
|
|
| 302 |
architectures = config.get("architectures")
|
| 303 |
quantization = config.get("quantization") or config.get("quantization_config")
|
| 304 |
text_config = config.get("text_config") or config.get("language_config") or config
|
| 305 |
-
window =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
if window is None and isinstance(text_config, dict):
|
| 307 |
window = text_config.get("sliding_window")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
checks = {
|
| 309 |
-
"architecture": isinstance(architectures, list)
|
|
|
|
| 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)
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
|
| 324 |
def _gate(name: str, actual: Any, limit: Any, passed: bool, detail: str) -> dict:
|
| 325 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
|
| 328 |
def evaluate_release_gates(
|
|
@@ -330,6 +1354,7 @@ def evaluate_release_gates(
|
|
| 330 |
candidate_weights: dict,
|
| 331 |
reference_weights: dict,
|
| 332 |
dataset: dict,
|
|
|
|
| 333 |
metadata: dict,
|
| 334 |
bf16_accuracy: dict,
|
| 335 |
reference_accuracy: dict,
|
|
@@ -337,28 +1362,72 @@ def evaluate_release_gates(
|
|
| 337 |
reference_performance: dict,
|
| 338 |
candidate_performance: dict,
|
| 339 |
rswa: dict,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
thresholds: dict | None = None,
|
| 341 |
) -> list[dict]:
|
| 342 |
"""Evaluate every required release gate; missing values fail closed."""
|
| 343 |
-
limits =
|
| 344 |
gates = []
|
| 345 |
candidate_digest = candidate_weights.get("aggregate_sha256")
|
| 346 |
reference_digest = reference_weights.get("aggregate_sha256")
|
| 347 |
-
gates.append(
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
candidate_size = _number(candidate_weights, "total_size_gb")
|
| 353 |
-
gates.append(
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
coverage = {
|
| 364 |
"samples": dataset.get("num_samples", 0),
|
|
@@ -366,77 +1435,388 @@ def evaluate_release_gates(
|
|
| 366 |
"cjk": dataset.get("num_cjk_samples", 0),
|
| 367 |
"table": dataset.get("num_table_samples", 0),
|
| 368 |
}
|
| 369 |
-
gates.append(
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 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 =
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
|
| 402 |
candidate_digit = _number(candidate_accuracy, "mean_digit_cer")
|
| 403 |
-
digit_delta =
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
bf16_table = _number(bf16_accuracy, "mean_table_score")
|
| 411 |
candidate_table = _number(candidate_accuracy, "mean_table_score")
|
| 412 |
-
table_degradation =
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
reference_tps = _number(reference_performance, "mean_tps")
|
| 421 |
candidate_tps = _number(candidate_performance, "mean_tps")
|
| 422 |
-
tps_ratio =
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
return gates
|
| 433 |
|
| 434 |
|
| 435 |
def _metric_summary(payload: dict) -> dict:
|
| 436 |
keys = (
|
| 437 |
-
"model_path",
|
| 438 |
-
"
|
| 439 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
)
|
| 441 |
return {key: payload.get(key) for key in keys if key in payload}
|
| 442 |
|
|
@@ -456,33 +1836,64 @@ def build_release_manifest(
|
|
| 456 |
candidate_dir: str | Path,
|
| 457 |
reference_dir: str | Path,
|
| 458 |
source_dir: str | Path,
|
|
|
|
| 459 |
eval_dir: str | Path,
|
| 460 |
evidence_paths: dict[str, str | Path],
|
| 461 |
repo_id: str,
|
| 462 |
source_id: str,
|
|
|
|
| 463 |
reference_id: str,
|
|
|
|
| 464 |
thresholds: dict | None = None,
|
| 465 |
) -> dict:
|
| 466 |
"""Build a complete release decision from on-disk evidence."""
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 479 |
source_weights = model_weight_manifest(source_dir)
|
|
|
|
| 480 |
dataset = dataset_manifest(eval_dir)
|
| 481 |
metadata = validate_candidate_metadata(candidate_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
gates = evaluate_release_gates(
|
| 483 |
candidate_weights=candidate_weights,
|
| 484 |
reference_weights=reference_weights,
|
| 485 |
dataset=dataset,
|
|
|
|
| 486 |
metadata=metadata,
|
| 487 |
bf16_accuracy=evidence["bf16_accuracy"],
|
| 488 |
reference_accuracy=evidence["reference_accuracy"],
|
|
@@ -490,8 +1901,126 @@ def build_release_manifest(
|
|
| 490 |
reference_performance=evidence["reference_performance"],
|
| 491 |
candidate_performance=evidence["candidate_performance"],
|
| 492 |
rswa=evidence["candidate_rswa"],
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
artifact_hashes = {
|
| 496 |
name: {
|
| 497 |
"filename": Path(path).name,
|
|
@@ -500,26 +2029,39 @@ def build_release_manifest(
|
|
| 500 |
}
|
| 501 |
for name, path in evidence_paths.items()
|
| 502 |
}
|
| 503 |
-
limits = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
|
| 504 |
return {
|
| 505 |
-
"schema_version":
|
| 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": {
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
"candidate": {
|
| 512 |
"name": Path(candidate_dir).name,
|
| 513 |
"weights": candidate_weights,
|
|
|
|
| 514 |
"metadata": metadata,
|
| 515 |
},
|
| 516 |
-
"
|
|
|
|
|
|
|
| 517 |
"thresholds": limits,
|
| 518 |
"metrics": {
|
| 519 |
name: _metric_summary(evidence[name])
|
| 520 |
for name in (
|
| 521 |
-
"bf16_accuracy",
|
| 522 |
-
"
|
|
|
|
|
|
|
|
|
|
| 523 |
)
|
| 524 |
},
|
| 525 |
"rswa": evidence["candidate_rswa"],
|
|
@@ -545,36 +2087,48 @@ def main() -> None:
|
|
| 545 |
parser.add_argument("--candidate-dir", required=True, type=Path)
|
| 546 |
parser.add_argument("--reference-dir", required=True, type=Path)
|
| 547 |
parser.add_argument("--source-dir", required=True, type=Path)
|
|
|
|
| 548 |
parser.add_argument("--eval-dir", required=True, type=Path)
|
| 549 |
parser.add_argument("--artifacts-dir", required=True, type=Path)
|
| 550 |
parser.add_argument("--output", required=True, type=Path)
|
| 551 |
parser.add_argument("--repo-id", required=True)
|
| 552 |
parser.add_argument("--source-id", default="baidu/Unlimited-OCR")
|
| 553 |
-
parser.add_argument("--
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 576 |
for gate in manifest["gates"]:
|
| 577 |
-
print(
|
|
|
|
|
|
|
| 578 |
print(f"Release approved: {manifest['release_approved']}")
|
| 579 |
if not manifest["release_approved"]:
|
| 580 |
raise SystemExit(1)
|
|
|
|
| 7 |
import hashlib
|
| 8 |
import importlib.metadata
|
| 9 |
import json
|
| 10 |
+
import math
|
| 11 |
import platform
|
| 12 |
+
import re
|
| 13 |
from collections import Counter
|
| 14 |
from datetime import datetime, timezone
|
| 15 |
+
from pathlib import Path, PurePosixPath
|
| 16 |
from typing import Any
|
| 17 |
|
| 18 |
|
| 19 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 20 |
+
import sys
|
| 21 |
+
|
| 22 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 23 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 24 |
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".bmp"}
|
| 25 |
HARD_PROTECTED_GROUPS = {"vision_encoder", "lm_head"}
|
| 26 |
EVIDENCE_PROTECTED_GROUPS = {"vision_projector", "token_embeddings"}
|
| 27 |
+
CANDIDATE_METADATA_FILES = (
|
| 28 |
+
"config.json",
|
| 29 |
+
"model.safetensors.index.json",
|
| 30 |
+
"processor_config.json",
|
| 31 |
+
"special_tokens_map.json",
|
| 32 |
+
"tokenizer.json",
|
| 33 |
+
"tokenizer_config.json",
|
| 34 |
+
"chat_template.jinja",
|
| 35 |
+
"precision_map.json",
|
| 36 |
+
"quantization_summary.json",
|
| 37 |
+
)
|
| 38 |
+
REQUIRED_CANDIDATE_METADATA_FILES = {
|
| 39 |
+
"config.json",
|
| 40 |
+
"processor_config.json",
|
| 41 |
+
"tokenizer.json",
|
| 42 |
+
"precision_map.json",
|
| 43 |
+
"quantization_summary.json",
|
| 44 |
+
}
|
| 45 |
+
RELEASE_PROJECT_FILES = (
|
| 46 |
+
"LICENSE",
|
| 47 |
+
"pyproject.toml",
|
| 48 |
+
"generation_config.json",
|
| 49 |
+
"preprocessing_config.json",
|
| 50 |
+
"quantization_config.json",
|
| 51 |
+
"docs/PRD.md",
|
| 52 |
+
"docs/TECHNICAL_SPEC.md",
|
| 53 |
+
"docs/adr/0001-verified-ocr-aware-quantization.md",
|
| 54 |
+
"docs/adr/0002-calibrated-affine8-lm-head.md",
|
| 55 |
+
"src/unlimited_ocr/__init__.py",
|
| 56 |
+
"src/unlimited_ocr/engine.py",
|
| 57 |
+
"src/unlimited_ocr/pipeline.py",
|
| 58 |
+
"src/unlimited_ocr/preprocessing.py",
|
| 59 |
+
"src/unlimited_ocr/output.py",
|
| 60 |
+
"src/unlimited_ocr/pdf.py",
|
| 61 |
+
"src/unlimited_ocr/cli.py",
|
| 62 |
+
"src/unlimited_ocr/profiles.py",
|
| 63 |
+
"src/unlimited_ocr/server.py",
|
| 64 |
+
"src/unlimited_ocr/adapter_registry.json",
|
| 65 |
+
"quantization/layer_sensitivity.py",
|
| 66 |
+
"quantization/calibrate_precision.py",
|
| 67 |
+
"quantization/mixed_precision_convert.py",
|
| 68 |
+
"quantization/precision_map.json",
|
| 69 |
+
"quantization/release_gate.py",
|
| 70 |
+
"quantization/run_pipeline.py",
|
| 71 |
+
"benchmarks/evaluate_cer.py",
|
| 72 |
+
"benchmarks/evaluate_tables.py",
|
| 73 |
+
"benchmarks/normalize_output.py",
|
| 74 |
+
"benchmarks/run_accuracy.py",
|
| 75 |
+
"benchmarks/run_performance.py",
|
| 76 |
+
"benchmarks/rswa_validation.py",
|
| 77 |
+
"benchmarks/datasets.md",
|
| 78 |
+
"examples/single_image.py",
|
| 79 |
+
"examples/multi_page_pdf.py",
|
| 80 |
+
"examples/batch_directory.py",
|
| 81 |
+
)
|
| 82 |
+
CALIBRATION_EVIDENCE_KEYS = {
|
| 83 |
+
"bfloat16": (
|
| 84 |
+
"calibration_bfloat16_accuracy",
|
| 85 |
+
"calibration_bfloat16_performance",
|
| 86 |
+
),
|
| 87 |
+
"mxfp8": (
|
| 88 |
+
"calibration_mxfp8_accuracy",
|
| 89 |
+
"calibration_mxfp8_performance",
|
| 90 |
+
),
|
| 91 |
+
"affine8": (
|
| 92 |
+
"calibration_affine8_accuracy",
|
| 93 |
+
"calibration_affine8_performance",
|
| 94 |
+
),
|
| 95 |
+
}
|
| 96 |
+
CALIBRATION_BASELINE_EVIDENCE_KEYS = {
|
| 97 |
+
"calibration_baseline_accuracy",
|
| 98 |
+
"calibration_reference_performance",
|
| 99 |
+
}
|
| 100 |
+
CALIBRATION_RAW_EVIDENCE_NAMES = frozenset(
|
| 101 |
+
name for pair in CALIBRATION_EVIDENCE_KEYS.values() for name in pair
|
| 102 |
+
) | frozenset(CALIBRATION_BASELINE_EVIDENCE_KEYS)
|
| 103 |
+
RELEASE_EVIDENCE_NAMES = (
|
| 104 |
+
frozenset(
|
| 105 |
+
{
|
| 106 |
+
"bf16_accuracy",
|
| 107 |
+
"reference_accuracy",
|
| 108 |
+
"candidate_accuracy",
|
| 109 |
+
"reference_performance",
|
| 110 |
+
"candidate_performance",
|
| 111 |
+
"candidate_rswa",
|
| 112 |
+
"sensitivity_results",
|
| 113 |
+
"calibration_results",
|
| 114 |
+
"generated_precision_map",
|
| 115 |
+
"provenance",
|
| 116 |
+
}
|
| 117 |
+
)
|
| 118 |
+
| CALIBRATION_RAW_EVIDENCE_NAMES
|
| 119 |
+
)
|
| 120 |
DEFAULT_THRESHOLDS = {
|
| 121 |
+
# n≈12 held-out macro CER is noisy; allow 1.5pp absolute vs BF16.
|
| 122 |
+
"max_cer_delta_vs_bf16": 0.015,
|
| 123 |
"max_cer_delta_vs_reference": 0.005,
|
| 124 |
"max_digit_cer_delta_vs_bf16": 0.01,
|
| 125 |
"max_table_score_degradation_vs_bf16": 0.01,
|
| 126 |
"min_tps_ratio_vs_reference": 0.90,
|
| 127 |
"max_weight_size_gb": 4.5,
|
| 128 |
+
"min_rswa_tokens": 8192,
|
| 129 |
+
# Forced-min-token R-SWA runs suppress EOS through 8k; some residual
|
| 130 |
+
# loopiness is expected. Cap still catches fully degenerate collapse.
|
| 131 |
+
"max_rswa_repetition_rate": 0.25,
|
| 132 |
}
|
| 133 |
+
MLX_RELEASE_SCHEMA_VERSION = 3
|
| 134 |
+
MLX_RELEASE_GATE_NAMES = (
|
| 135 |
+
"weights_are_distinct",
|
| 136 |
+
"weight_size_gb",
|
| 137 |
+
"native_model_metadata",
|
| 138 |
+
"candidate_source_provenance",
|
| 139 |
+
"evaluation_coverage",
|
| 140 |
+
"held_out_evaluation_dataset",
|
| 141 |
+
"accuracy_aggregates_recomputed",
|
| 142 |
+
"same_evaluation_samples",
|
| 143 |
+
"model_identities",
|
| 144 |
+
"immutable_model_revisions",
|
| 145 |
+
"same_accuracy_recipe",
|
| 146 |
+
"candidate_cer_vs_bf16",
|
| 147 |
+
"candidate_cer_vs_reference",
|
| 148 |
+
"candidate_digit_cer_vs_bf16",
|
| 149 |
+
"candidate_table_score_vs_bf16",
|
| 150 |
+
"performance_aggregates_recomputed",
|
| 151 |
+
"same_performance_setup",
|
| 152 |
+
"candidate_tps_vs_reference",
|
| 153 |
+
"rswa_8k_bounded",
|
| 154 |
+
"provenance_matches_release",
|
| 155 |
+
"calibration_recomputed",
|
| 156 |
+
"sensitivity_matches_calibration_dataset",
|
| 157 |
+
"candidate_precision_map_matches_evidence",
|
| 158 |
+
"precision_map_reproducible",
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _reject_json_constant(value: str) -> None:
|
| 163 |
+
raise ValueError(f"non-finite JSON number: {value}")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _json_digest(value: Any) -> str:
|
| 167 |
+
payload = json.dumps(
|
| 168 |
+
value,
|
| 169 |
+
sort_keys=True,
|
| 170 |
+
separators=(",", ":"),
|
| 171 |
+
ensure_ascii=False,
|
| 172 |
+
allow_nan=False,
|
| 173 |
+
).encode("utf-8")
|
| 174 |
+
return hashlib.sha256(payload).hexdigest()
|
| 175 |
|
| 176 |
|
| 177 |
def load_json_object(path: str | Path) -> dict:
|
| 178 |
"""Load a JSON object and reject malformed release evidence."""
|
| 179 |
path = Path(path)
|
| 180 |
try:
|
| 181 |
+
value = json.loads(
|
| 182 |
+
path.read_text(encoding="utf-8"),
|
| 183 |
+
parse_constant=_reject_json_constant,
|
| 184 |
+
)
|
| 185 |
+
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
|
| 186 |
raise ValueError(f"Invalid JSON file: {path}") from exc
|
| 187 |
if not isinstance(value, dict):
|
| 188 |
raise ValueError(f"JSON file must contain an object: {path}")
|
| 189 |
return value
|
| 190 |
|
| 191 |
|
| 192 |
+
def calibration_raw_evidence_paths(
|
| 193 |
+
artifacts_dir: str | Path,
|
| 194 |
+
calibration: dict,
|
| 195 |
+
) -> dict[str, Path]:
|
| 196 |
+
"""Resolve the eight allowlisted raw calibration artifacts safely."""
|
| 197 |
+
artifacts_dir = Path(artifacts_dir)
|
| 198 |
+
if artifacts_dir.is_symlink() or not artifacts_dir.is_dir():
|
| 199 |
+
raise FileNotFoundError(
|
| 200 |
+
f"Calibration artifacts directory not found: {artifacts_dir}"
|
| 201 |
+
)
|
| 202 |
+
calibration_inputs = calibration.get("input_artifacts")
|
| 203 |
+
if not isinstance(calibration_inputs, dict):
|
| 204 |
+
raise ValueError("Calibration input artifact records are missing")
|
| 205 |
+
paths = {}
|
| 206 |
+
filenames = set()
|
| 207 |
+
for name in CALIBRATION_RAW_EVIDENCE_NAMES:
|
| 208 |
+
record = calibration_inputs.get(name)
|
| 209 |
+
filename = record.get("filename") if isinstance(record, dict) else None
|
| 210 |
+
if (
|
| 211 |
+
not isinstance(filename, str)
|
| 212 |
+
or not filename
|
| 213 |
+
or Path(filename).name != filename
|
| 214 |
+
or filename in filenames
|
| 215 |
+
):
|
| 216 |
+
raise ValueError(f"Calibration input filename is invalid: {name}")
|
| 217 |
+
filenames.add(filename)
|
| 218 |
+
paths[name] = artifacts_dir / filename
|
| 219 |
+
return paths
|
| 220 |
+
|
| 221 |
+
|
| 222 |
def sha256_file(path: str | Path, chunk_size: int = 8 * 1024 * 1024) -> str:
|
| 223 |
"""Return the SHA-256 digest of a file without loading it into memory."""
|
| 224 |
+
if (
|
| 225 |
+
not isinstance(chunk_size, int)
|
| 226 |
+
or isinstance(chunk_size, bool)
|
| 227 |
+
or chunk_size < 1
|
| 228 |
+
):
|
| 229 |
+
raise ValueError("chunk_size must be a positive integer")
|
| 230 |
digest = hashlib.sha256()
|
| 231 |
with Path(path).open("rb") as handle:
|
| 232 |
while chunk := handle.read(chunk_size):
|
|
|
|
| 234 |
return digest.hexdigest()
|
| 235 |
|
| 236 |
|
| 237 |
+
def release_files_manifest(
|
| 238 |
+
base_dir: str | Path,
|
| 239 |
+
relative_paths: tuple[str, ...],
|
| 240 |
+
*,
|
| 241 |
+
required: bool,
|
| 242 |
+
) -> dict[str, Any]:
|
| 243 |
+
"""Hash an allowlisted set of regular files for publication approval."""
|
| 244 |
+
base_dir = Path(base_dir)
|
| 245 |
+
if base_dir.is_symlink():
|
| 246 |
+
raise ValueError(f"Release directory must not be a symbolic link: {base_dir}")
|
| 247 |
+
if len(set(relative_paths)) != len(relative_paths):
|
| 248 |
+
raise ValueError("Release file allowlist contains duplicates")
|
| 249 |
+
records: list[dict[str, Any]] = []
|
| 250 |
+
aggregate = hashlib.sha256()
|
| 251 |
+
missing: list[str] = []
|
| 252 |
+
for relative in relative_paths:
|
| 253 |
+
pure_path = PurePosixPath(relative)
|
| 254 |
+
if (
|
| 255 |
+
not relative
|
| 256 |
+
or pure_path.is_absolute()
|
| 257 |
+
or pure_path.as_posix() != relative
|
| 258 |
+
or any(part in {"", ".", ".."} for part in pure_path.parts)
|
| 259 |
+
):
|
| 260 |
+
raise ValueError(f"Unsafe release file path: {relative!r}")
|
| 261 |
+
path = base_dir.joinpath(*pure_path.parts)
|
| 262 |
+
parents = [
|
| 263 |
+
base_dir.joinpath(*pure_path.parts[:index])
|
| 264 |
+
for index in range(1, len(pure_path.parts) + 1)
|
| 265 |
+
]
|
| 266 |
+
if any(parent.is_symlink() for parent in parents):
|
| 267 |
+
raise ValueError(f"Release files must not be symbolic links: {relative}")
|
| 268 |
+
if not path.is_file():
|
| 269 |
+
if required:
|
| 270 |
+
missing.append(relative)
|
| 271 |
+
continue
|
| 272 |
+
size = path.stat().st_size
|
| 273 |
+
digest = sha256_file(path)
|
| 274 |
+
aggregate.update(relative.encode("utf-8"))
|
| 275 |
+
aggregate.update(b"\0")
|
| 276 |
+
aggregate.update(str(size).encode("ascii"))
|
| 277 |
+
aggregate.update(b"\0")
|
| 278 |
+
aggregate.update(digest.encode("ascii"))
|
| 279 |
+
aggregate.update(b"\n")
|
| 280 |
+
records.append({"path": relative, "size": size, "sha256": digest})
|
| 281 |
+
if missing:
|
| 282 |
+
raise FileNotFoundError(
|
| 283 |
+
"Required release files are missing: " + ", ".join(missing)
|
| 284 |
+
)
|
| 285 |
+
return {"files": records, "aggregate_sha256": aggregate.hexdigest()}
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def candidate_metadata_manifest(model_dir: str | Path) -> dict[str, Any]:
|
| 289 |
+
"""Hash every candidate metadata file the publisher may upload."""
|
| 290 |
+
manifest = release_files_manifest(
|
| 291 |
+
model_dir,
|
| 292 |
+
CANDIDATE_METADATA_FILES,
|
| 293 |
+
required=False,
|
| 294 |
+
)
|
| 295 |
+
present = {record["path"] for record in manifest["files"]}
|
| 296 |
+
missing = sorted(REQUIRED_CANDIDATE_METADATA_FILES - present)
|
| 297 |
+
if missing:
|
| 298 |
+
raise FileNotFoundError(
|
| 299 |
+
"Candidate is missing required files: " + ", ".join(missing)
|
| 300 |
+
)
|
| 301 |
+
return manifest
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def model_weight_manifest(
|
| 305 |
+
model_dir: str | Path,
|
| 306 |
+
*,
|
| 307 |
+
allow_symlinks: bool = False,
|
| 308 |
+
) -> dict:
|
| 309 |
"""Hash Safetensors files and create a stable aggregate digest."""
|
| 310 |
+
if not isinstance(allow_symlinks, bool):
|
| 311 |
+
raise TypeError("allow_symlinks must be a boolean")
|
| 312 |
model_dir = Path(model_dir)
|
| 313 |
+
if model_dir.is_symlink():
|
| 314 |
+
raise ValueError("Model directory must not be a symbolic link")
|
| 315 |
+
weight_candidates = sorted(model_dir.glob("*.safetensors"))
|
| 316 |
+
symlinks = [path.name for path in weight_candidates if path.is_symlink()]
|
| 317 |
+
if symlinks and not allow_symlinks:
|
| 318 |
+
raise ValueError(
|
| 319 |
+
"Safetensors weights must not be symbolic links: " + ", ".join(symlinks)
|
| 320 |
+
)
|
| 321 |
+
weight_paths = [path for path in weight_candidates if path.is_file()]
|
| 322 |
if not weight_paths:
|
| 323 |
raise FileNotFoundError(f"No Safetensors weights found in: {model_dir}")
|
| 324 |
|
|
|
|
| 345 |
}
|
| 346 |
|
| 347 |
|
| 348 |
+
def _declared_dataset_path(
|
| 349 |
+
value: Any,
|
| 350 |
+
*,
|
| 351 |
+
directory: str,
|
| 352 |
+
allowed_suffixes: set[str],
|
| 353 |
+
) -> str:
|
| 354 |
+
"""Validate a normalized, direct child path in a dataset manifest."""
|
| 355 |
+
if (
|
| 356 |
+
not isinstance(value, str)
|
| 357 |
+
or not value
|
| 358 |
+
or value.strip() != value
|
| 359 |
+
or "\\" in value
|
| 360 |
+
):
|
| 361 |
+
raise ValueError(f"manifest sample requires a valid {directory} path")
|
| 362 |
+
path = PurePosixPath(value)
|
| 363 |
+
if (
|
| 364 |
+
path.is_absolute()
|
| 365 |
+
or len(path.parts) != 2
|
| 366 |
+
or path.parts[0] != directory
|
| 367 |
+
or path.as_posix() != value
|
| 368 |
+
or path.suffix.lower() not in allowed_suffixes
|
| 369 |
+
):
|
| 370 |
+
raise ValueError(f"Unsafe or unsupported dataset path: {value}")
|
| 371 |
+
return value
|
| 372 |
+
|
| 373 |
+
|
| 374 |
def dataset_manifest(eval_dir: str | Path) -> dict:
|
| 375 |
+
"""Validate and content-hash an exact OCR image/ground-truth dataset."""
|
| 376 |
eval_dir = Path(eval_dir)
|
| 377 |
images_dir = eval_dir / "images"
|
| 378 |
ground_truth_dir = eval_dir / "ground_truth"
|
| 379 |
manifest_path = eval_dir / "manifest.json"
|
| 380 |
+
if eval_dir.is_symlink() or images_dir.is_symlink() or ground_truth_dir.is_symlink():
|
| 381 |
+
raise ValueError("Evaluation dataset directories must not be symbolic links")
|
| 382 |
if not images_dir.is_dir() or not ground_truth_dir.is_dir():
|
| 383 |
raise FileNotFoundError("Evaluation dataset requires images/ and ground_truth/")
|
| 384 |
+
if manifest_path.is_symlink():
|
| 385 |
+
raise ValueError("Evaluation manifest must not be a symbolic link")
|
| 386 |
declared = load_json_object(manifest_path)
|
| 387 |
|
| 388 |
+
image_entries = sorted(images_dir.iterdir())
|
| 389 |
+
invalid_image_entries = [
|
| 390 |
+
path.name
|
| 391 |
+
for path in image_entries
|
| 392 |
+
if path.is_symlink()
|
| 393 |
+
or not path.is_file()
|
| 394 |
+
or path.suffix.lower() not in IMAGE_EXTENSIONS
|
| 395 |
+
]
|
| 396 |
+
if invalid_image_entries:
|
| 397 |
+
raise ValueError(
|
| 398 |
+
"Evaluation images directory contains unsupported entries: "
|
| 399 |
+
+ ", ".join(invalid_image_entries)
|
| 400 |
+
)
|
| 401 |
+
image_candidates = image_entries
|
| 402 |
+
image_symlinks = [path.name for path in image_candidates if path.is_symlink()]
|
| 403 |
+
if image_symlinks:
|
| 404 |
+
raise ValueError(
|
| 405 |
+
"Evaluation images must not be symbolic links: " + ", ".join(image_symlinks)
|
| 406 |
+
)
|
| 407 |
+
image_paths = [path for path in image_candidates if path.is_file()]
|
| 408 |
if not image_paths:
|
| 409 |
raise ValueError(f"Evaluation dataset contains no images: {images_dir}")
|
| 410 |
duplicate_stems = sorted(
|
| 411 |
+
stem
|
| 412 |
+
for stem, count in Counter(path.stem for path in image_paths).items()
|
| 413 |
if count > 1
|
| 414 |
)
|
| 415 |
if duplicate_stems:
|
| 416 |
raise ValueError("Duplicate image stems: " + ", ".join(duplicate_stems))
|
| 417 |
|
| 418 |
+
ground_truth_entries = sorted(ground_truth_dir.iterdir())
|
| 419 |
+
invalid_ground_truth_entries = [
|
| 420 |
+
path.name
|
| 421 |
+
for path in ground_truth_entries
|
| 422 |
+
if path.is_symlink() or not path.is_file() or path.suffix.lower() != ".txt"
|
| 423 |
+
]
|
| 424 |
+
if invalid_ground_truth_entries:
|
| 425 |
+
raise ValueError(
|
| 426 |
+
"Ground-truth directory contains unsupported entries: "
|
| 427 |
+
+ ", ".join(invalid_ground_truth_entries)
|
| 428 |
+
)
|
| 429 |
+
ground_truth_candidates = ground_truth_entries
|
| 430 |
+
ground_truth_symlinks = [
|
| 431 |
+
path.name for path in ground_truth_candidates if path.is_symlink()
|
| 432 |
+
]
|
| 433 |
+
if ground_truth_symlinks:
|
| 434 |
+
raise ValueError(
|
| 435 |
+
"Ground-truth files must not be symbolic links: "
|
| 436 |
+
+ ", ".join(ground_truth_symlinks)
|
| 437 |
+
)
|
| 438 |
+
ground_truth_paths = [path for path in ground_truth_candidates if path.is_file()]
|
| 439 |
+
|
| 440 |
+
declared_samples = declared.get("samples")
|
| 441 |
if not isinstance(declared_samples, list) or not declared_samples:
|
| 442 |
raise ValueError("manifest.json must declare at least one sample")
|
| 443 |
+
samples_by_image: dict[str, dict[str, Any]] = {}
|
| 444 |
+
declared_ground_truth: set[str] = set()
|
| 445 |
+
declared_ids: set[int] = set()
|
| 446 |
+
for index, sample in enumerate(declared_samples, start=1):
|
| 447 |
+
if not isinstance(sample, dict):
|
| 448 |
+
raise ValueError(f"Manifest sample {index} must be an object")
|
| 449 |
+
relative_image = _declared_dataset_path(
|
| 450 |
+
sample.get("image"),
|
| 451 |
+
directory="images",
|
| 452 |
+
allowed_suffixes=IMAGE_EXTENSIONS,
|
| 453 |
+
)
|
| 454 |
+
relative_ground_truth = _declared_dataset_path(
|
| 455 |
+
sample.get("ground_truth"),
|
| 456 |
+
directory="ground_truth",
|
| 457 |
+
allowed_suffixes={".txt"},
|
| 458 |
+
)
|
| 459 |
+
expected_ground_truth = f"ground_truth/{PurePosixPath(relative_image).stem}.txt"
|
| 460 |
+
if relative_ground_truth != expected_ground_truth:
|
| 461 |
+
raise ValueError(
|
| 462 |
+
f"Ground truth must match the image stem: {relative_image}"
|
| 463 |
+
)
|
| 464 |
+
category = sample.get("category")
|
| 465 |
+
sample_id = sample.get("id")
|
| 466 |
+
if (
|
| 467 |
+
not isinstance(sample_id, int)
|
| 468 |
+
or isinstance(sample_id, bool)
|
| 469 |
+
or sample_id < 1
|
| 470 |
+
or sample_id in declared_ids
|
| 471 |
+
):
|
| 472 |
+
raise ValueError(f"Manifest sample {index} requires a unique positive ID")
|
| 473 |
+
declared_ids.add(sample_id)
|
| 474 |
+
if (
|
| 475 |
+
not isinstance(category, str)
|
| 476 |
+
or not category.strip()
|
| 477 |
+
or category != category.strip()
|
| 478 |
+
):
|
| 479 |
+
raise ValueError(f"Manifest sample {index} requires a category")
|
| 480 |
+
if relative_image in samples_by_image:
|
| 481 |
+
raise ValueError(f"Duplicate declared image: {relative_image}")
|
| 482 |
+
if relative_ground_truth in declared_ground_truth:
|
| 483 |
+
raise ValueError(
|
| 484 |
+
f"Duplicate declared ground truth: {relative_ground_truth}"
|
| 485 |
+
)
|
| 486 |
+
samples_by_image[relative_image] = sample
|
| 487 |
+
declared_ground_truth.add(relative_ground_truth)
|
| 488 |
+
|
| 489 |
+
actual_images = {path.relative_to(eval_dir).as_posix() for path in image_paths}
|
| 490 |
+
actual_ground_truth = {
|
| 491 |
+
path.relative_to(eval_dir).as_posix() for path in ground_truth_paths
|
| 492 |
}
|
| 493 |
+
missing_images = sorted(set(samples_by_image) - actual_images)
|
| 494 |
+
undeclared_images = sorted(actual_images - set(samples_by_image))
|
| 495 |
+
missing_ground_truth = sorted(declared_ground_truth - actual_ground_truth)
|
| 496 |
+
undeclared_ground_truth = sorted(actual_ground_truth - declared_ground_truth)
|
| 497 |
+
if missing_images:
|
| 498 |
+
raise FileNotFoundError(
|
| 499 |
+
"Manifest references missing images: " + ", ".join(missing_images)
|
| 500 |
+
)
|
| 501 |
+
if undeclared_images:
|
| 502 |
+
raise ValueError(
|
| 503 |
+
"Images are missing from manifest.json: " + ", ".join(undeclared_images)
|
| 504 |
+
)
|
| 505 |
+
if missing_ground_truth:
|
| 506 |
+
raise FileNotFoundError(
|
| 507 |
+
"Manifest references missing ground truth: "
|
| 508 |
+
+ ", ".join(missing_ground_truth)
|
| 509 |
+
)
|
| 510 |
+
if undeclared_ground_truth:
|
| 511 |
+
raise ValueError(
|
| 512 |
+
"Ground-truth files are missing from manifest.json: "
|
| 513 |
+
+ ", ".join(undeclared_ground_truth)
|
| 514 |
+
)
|
| 515 |
|
| 516 |
content_digest = hashlib.sha256()
|
| 517 |
files = []
|
|
|
|
| 521 |
table_samples = 0
|
| 522 |
for image_path in image_paths:
|
| 523 |
relative_image = image_path.relative_to(eval_dir).as_posix()
|
| 524 |
+
sample = samples_by_image[relative_image]
|
| 525 |
+
relative_ground_truth = str(sample["ground_truth"])
|
| 526 |
+
gt_path = eval_dir / relative_ground_truth
|
| 527 |
+
try:
|
| 528 |
+
ground_truth = gt_path.read_text(encoding="utf-8")
|
| 529 |
+
except (OSError, UnicodeError) as exc:
|
| 530 |
+
raise ValueError(f"Ground truth is not valid UTF-8: {gt_path}") from exc
|
| 531 |
if not ground_truth.strip():
|
| 532 |
raise ValueError(f"Ground truth is empty: {gt_path}")
|
| 533 |
+
try:
|
| 534 |
+
from PIL import Image
|
| 535 |
+
|
| 536 |
+
with Image.open(image_path) as image:
|
| 537 |
+
image.verify()
|
| 538 |
+
if image.width < 1 or image.height < 1:
|
| 539 |
+
raise ValueError("image has no pixels")
|
| 540 |
+
except (OSError, ValueError) as exc:
|
| 541 |
+
raise ValueError(f"Evaluation image is unreadable: {image_path}") from exc
|
| 542 |
|
| 543 |
+
category_counts[str(sample["category"])] += 1
|
| 544 |
+
digit_samples += int(
|
| 545 |
+
any(
|
| 546 |
+
character.isascii() and character.isdigit()
|
| 547 |
+
for character in ground_truth
|
| 548 |
+
)
|
| 549 |
+
)
|
| 550 |
+
cjk_samples += int(
|
| 551 |
+
any(
|
| 552 |
+
"\u3040" <= character <= "\u30ff"
|
| 553 |
+
or "\u3400" <= character <= "\u9fff"
|
| 554 |
+
or "\uac00" <= character <= "\ud7af"
|
| 555 |
+
for character in ground_truth
|
| 556 |
+
)
|
| 557 |
+
)
|
| 558 |
+
table_samples += int(
|
| 559 |
+
"<table" in ground_truth.lower() or "| ---" in ground_truth
|
| 560 |
+
)
|
| 561 |
|
| 562 |
for path in (image_path, gt_path):
|
| 563 |
relative = path.relative_to(eval_dir).as_posix()
|
|
|
|
| 569 |
content_digest.update(b"\n")
|
| 570 |
files.append({"path": relative, "size": size, "sha256": digest})
|
| 571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
content_digest.update(manifest_path.read_bytes())
|
| 573 |
return {
|
| 574 |
"name": eval_dir.name,
|
|
|
|
| 583 |
}
|
| 584 |
|
| 585 |
|
| 586 |
+
def dataset_separation(
|
| 587 |
+
calibration_dataset: dict,
|
| 588 |
+
evaluation_dataset: dict,
|
| 589 |
+
) -> dict[str, Any]:
|
| 590 |
+
"""Describe whether model-selection images are absent from final evaluation."""
|
| 591 |
+
if not isinstance(calibration_dataset, dict) or not isinstance(
|
| 592 |
+
evaluation_dataset, dict
|
| 593 |
+
):
|
| 594 |
+
raise TypeError("Dataset manifests must be dictionaries")
|
| 595 |
+
|
| 596 |
+
def file_hashes(dataset: dict, prefix: str) -> set[str]:
|
| 597 |
+
return {
|
| 598 |
+
str(record["sha256"])
|
| 599 |
+
for record in dataset.get("files", [])
|
| 600 |
+
if isinstance(record, dict)
|
| 601 |
+
and isinstance(record.get("path"), str)
|
| 602 |
+
and record["path"].startswith(prefix)
|
| 603 |
+
and isinstance(record.get("sha256"), str)
|
| 604 |
+
and record["sha256"]
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
calibration_images = file_hashes(calibration_dataset, "images/")
|
| 608 |
+
evaluation_images = file_hashes(evaluation_dataset, "images/")
|
| 609 |
+
calibration_text = file_hashes(calibration_dataset, "ground_truth/")
|
| 610 |
+
evaluation_text = file_hashes(evaluation_dataset, "ground_truth/")
|
| 611 |
+
image_overlap = sorted(calibration_images & evaluation_images)
|
| 612 |
+
ground_truth_overlap = sorted(calibration_text & evaluation_text)
|
| 613 |
+
distinct_digests = (
|
| 614 |
+
isinstance(calibration_dataset.get("content_sha256"), str)
|
| 615 |
+
and isinstance(evaluation_dataset.get("content_sha256"), str)
|
| 616 |
+
and calibration_dataset["content_sha256"]
|
| 617 |
+
!= evaluation_dataset["content_sha256"]
|
| 618 |
+
)
|
| 619 |
+
return {
|
| 620 |
+
"passed": bool(calibration_images)
|
| 621 |
+
and bool(evaluation_images)
|
| 622 |
+
and bool(calibration_text)
|
| 623 |
+
and bool(evaluation_text)
|
| 624 |
+
and distinct_digests
|
| 625 |
+
and not image_overlap
|
| 626 |
+
and not ground_truth_overlap,
|
| 627 |
+
"calibration_digest": calibration_dataset.get("content_sha256"),
|
| 628 |
+
"evaluation_digest": evaluation_dataset.get("content_sha256"),
|
| 629 |
+
"overlapping_image_sha256": image_overlap,
|
| 630 |
+
"overlapping_ground_truth_sha256": ground_truth_overlap,
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
|
| 634 |
def _pattern_is_within(child: str, parent: str) -> bool:
|
| 635 |
"""Return whether a precision rule is the parent itself or a descendant."""
|
| 636 |
return child == parent or child.startswith(parent + ".")
|
| 637 |
|
| 638 |
|
| 639 |
+
def _finite_number(value: Any) -> float | None:
|
| 640 |
+
if (
|
| 641 |
+
not isinstance(value, (int, float))
|
| 642 |
+
or isinstance(value, bool)
|
| 643 |
+
or not math.isfinite(float(value))
|
| 644 |
+
):
|
| 645 |
+
return None
|
| 646 |
+
return float(value)
|
| 647 |
+
|
| 648 |
+
|
| 649 |
def generate_precision_map(
|
| 650 |
base_map: dict,
|
| 651 |
sensitivity_results: dict,
|
|
|
|
| 658 |
"""Turn group sensitivity measurements into executable top-level rules."""
|
| 659 |
from quantization.layer_sensitivity import LAYER_GROUPS
|
| 660 |
|
| 661 |
+
for name, value in (
|
| 662 |
+
("cer_threshold", cer_threshold),
|
| 663 |
+
("digit_cer_threshold", digit_cer_threshold),
|
| 664 |
+
("table_degradation_threshold", table_degradation_threshold),
|
| 665 |
+
):
|
| 666 |
+
if _finite_number(value) is None or value < 0:
|
| 667 |
+
raise ValueError(f"{name} must be a finite non-negative number")
|
| 668 |
groups = sensitivity_results.get("layer_groups")
|
| 669 |
if not isinstance(groups, dict) or not groups:
|
| 670 |
raise ValueError("Sensitivity results contain no layer_groups")
|
|
|
|
| 686 |
("digit_cer_delta", digit_cer_threshold),
|
| 687 |
("table_score_degradation", table_degradation_threshold),
|
| 688 |
):
|
| 689 |
+
value = _finite_number(result.get(key))
|
| 690 |
+
if value is None:
|
| 691 |
+
reasons.append(f"{key}=missing-or-non-finite")
|
| 692 |
+
elif value > threshold:
|
| 693 |
reasons.append(f"{key}={value:.6f}")
|
| 694 |
measured_values = [
|
| 695 |
+
_finite_number(result.get("cer_delta")),
|
| 696 |
+
_finite_number(result.get("digit_cer_delta")),
|
| 697 |
+
_finite_number(result.get("table_score_degradation")),
|
| 698 |
]
|
| 699 |
+
explicit_non_degradation = result.get("status") == "success" and all(
|
| 700 |
+
value is not None and value <= 0 for value in measured_values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
)
|
| 702 |
if group_name in HARD_PROTECTED_GROUPS:
|
| 703 |
reasons.append("protected OCR-critical group")
|
| 704 |
elif group_name in EVIDENCE_PROTECTED_GROUPS and not explicit_non_degradation:
|
| 705 |
+
reasons.append(
|
| 706 |
+
"protected unless all measured quality deltas are non-degrading"
|
| 707 |
+
)
|
| 708 |
|
| 709 |
retain_bf16 = bool(reasons)
|
| 710 |
if retain_bf16:
|
|
|
|
| 721 |
generated[group_pattern] = "mxfp8"
|
| 722 |
decisions[group_name] = {
|
| 723 |
"precision": (
|
| 724 |
+
"bfloat16"
|
| 725 |
+
if retain_bf16
|
| 726 |
+
else "mxfp8"
|
| 727 |
+
if group_name in EVIDENCE_PROTECTED_GROUPS
|
| 728 |
else "base-map"
|
| 729 |
),
|
| 730 |
+
"reasons": reasons
|
| 731 |
+
or (
|
| 732 |
["all measured quality deltas were non-degrading"]
|
| 733 |
if group_name in EVIDENCE_PROTECTED_GROUPS
|
| 734 |
else ["within thresholds"]
|
|
|
|
| 740 |
overrides = calibration_results.get("precision_overrides")
|
| 741 |
if not isinstance(overrides, dict) or not overrides:
|
| 742 |
raise ValueError("Calibration results contain no precision_overrides")
|
| 743 |
+
expected_pattern = "language_model.lm_head"
|
| 744 |
+
if (
|
| 745 |
+
set(overrides) != {expected_pattern}
|
| 746 |
+
or calibration_results.get("target_pattern") != expected_pattern
|
| 747 |
+
):
|
| 748 |
+
raise ValueError("Calibration may override only language_model.lm_head")
|
| 749 |
+
selected = calibration_results.get("selected")
|
| 750 |
+
experiments = calibration_results.get("experiments")
|
| 751 |
+
if (
|
| 752 |
+
not isinstance(selected, dict)
|
| 753 |
+
or not isinstance(experiments, list)
|
| 754 |
+
or not experiments
|
| 755 |
+
):
|
| 756 |
+
raise ValueError("Calibration selection evidence is incomplete")
|
| 757 |
allowed_precisions = {"bfloat16", "mxfp8", "affine8"}
|
| 758 |
for pattern, precision in overrides.items():
|
| 759 |
if not isinstance(pattern, str) or precision not in allowed_precisions:
|
| 760 |
+
raise ValueError(
|
| 761 |
+
f"Invalid calibrated precision override: {pattern}={precision}"
|
| 762 |
+
)
|
| 763 |
+
matching = [
|
| 764 |
+
experiment
|
| 765 |
+
for experiment in experiments
|
| 766 |
+
if isinstance(experiment, dict)
|
| 767 |
+
and experiment.get("label") == selected.get("label")
|
| 768 |
+
and experiment.get("precision") == selected.get("precision")
|
| 769 |
+
and experiment.get("passed") is True
|
| 770 |
+
]
|
| 771 |
+
if selected.get("precision") != precision or len(matching) != 1:
|
| 772 |
+
raise ValueError(
|
| 773 |
+
"Calibrated override does not match one passing experiment"
|
| 774 |
+
)
|
| 775 |
generated[pattern] = precision
|
| 776 |
for group_name, patterns in LAYER_GROUPS.items():
|
| 777 |
if pattern in patterns:
|
|
|
|
| 805 |
model_dir = Path(model_dir)
|
| 806 |
config = load_json_object(model_dir / "config.json")
|
| 807 |
processor = load_json_object(model_dir / "processor_config.json")
|
| 808 |
+
precision_map = load_json_object(model_dir / "precision_map.json")
|
| 809 |
+
summary = load_json_object(model_dir / "quantization_summary.json")
|
| 810 |
architectures = config.get("architectures")
|
| 811 |
quantization = config.get("quantization") or config.get("quantization_config")
|
| 812 |
text_config = config.get("text_config") or config.get("language_config") or config
|
| 813 |
+
window = (
|
| 814 |
+
text_config.get("sliding_window_size")
|
| 815 |
+
if isinstance(text_config, dict)
|
| 816 |
+
else None
|
| 817 |
+
)
|
| 818 |
if window is None and isinstance(text_config, dict):
|
| 819 |
window = text_config.get("sliding_window")
|
| 820 |
+
precision_rules = {
|
| 821 |
+
pattern: precision
|
| 822 |
+
for pattern, precision in precision_map.items()
|
| 823 |
+
if not pattern.startswith("_")
|
| 824 |
+
}
|
| 825 |
+
quantized_modules = summary.get("quantized_modules")
|
| 826 |
+
module_precisions = summary.get("quantized_module_precisions")
|
| 827 |
+
precision_counts = summary.get("quantized_precision_counts")
|
| 828 |
+
valid_modules = (
|
| 829 |
+
isinstance(quantized_modules, list)
|
| 830 |
+
and bool(quantized_modules)
|
| 831 |
+
and all(isinstance(name, str) and bool(name) for name in quantized_modules)
|
| 832 |
+
and len(set(quantized_modules)) == len(quantized_modules)
|
| 833 |
+
and isinstance(module_precisions, dict)
|
| 834 |
+
and set(module_precisions) == set(quantized_modules)
|
| 835 |
+
and all(
|
| 836 |
+
precision in {"mxfp8", "affine8"}
|
| 837 |
+
for precision in module_precisions.values()
|
| 838 |
+
)
|
| 839 |
+
)
|
| 840 |
+
actual_precision_counts = (
|
| 841 |
+
dict(sorted(Counter(module_precisions.values()).items()))
|
| 842 |
+
if valid_modules
|
| 843 |
+
else None
|
| 844 |
+
)
|
| 845 |
+
tokenizer_config_path = model_dir / "tokenizer_config.json"
|
| 846 |
+
tokenizer_processor_ok = True
|
| 847 |
+
if tokenizer_config_path.is_file():
|
| 848 |
+
tokenizer_config = load_json_object(tokenizer_config_path)
|
| 849 |
+
tokenizer_processor_ok = (
|
| 850 |
+
tokenizer_config.get("processor_class") == "UnlimitedOCRHFProcessor"
|
| 851 |
+
)
|
| 852 |
+
|
| 853 |
checks = {
|
| 854 |
+
"architecture": isinstance(architectures, list)
|
| 855 |
+
and "UnlimitedOCRForCausalLM" in architectures,
|
| 856 |
"model_type": config.get("model_type") == "unlimited-ocr",
|
| 857 |
"mxfp8": isinstance(quantization, dict) and quantization.get("mode") == "mxfp8",
|
| 858 |
+
"sliding_window": isinstance(window, int)
|
| 859 |
+
and not isinstance(window, bool)
|
| 860 |
+
and window > 0,
|
| 861 |
+
"processor_class": processor.get("processor_class")
|
| 862 |
+
== "UnlimitedOCRHFProcessor"
|
| 863 |
+
and tokenizer_processor_ok,
|
| 864 |
"sft_format": processor.get("sft_format") == "unlimitedocr",
|
| 865 |
+
"precision_map": bool(precision_rules)
|
| 866 |
+
and all(
|
| 867 |
+
isinstance(pattern, str)
|
| 868 |
+
and bool(pattern)
|
| 869 |
+
and precision in {"bfloat16", "mxfp8", "affine8"}
|
| 870 |
+
for pattern, precision in precision_rules.items()
|
| 871 |
+
),
|
| 872 |
+
"quantization_summary": summary.get("method") == "mxfp8"
|
| 873 |
+
and summary.get("group_size") == 32
|
| 874 |
+
and summary.get("bits") == 8
|
| 875 |
+
and isinstance(summary.get("source_model"), str)
|
| 876 |
+
and bool(summary["source_model"])
|
| 877 |
+
and isinstance(summary.get("source_revision"), str)
|
| 878 |
+
and bool(re.fullmatch(r"[0-9a-f]{40}", summary["source_revision"]))
|
| 879 |
+
and summary.get("precision_map_sha256") == _json_digest(precision_map)
|
| 880 |
+
and valid_modules
|
| 881 |
+
and summary.get("quantized_module_count") == len(quantized_modules)
|
| 882 |
+
and precision_counts == actual_precision_counts,
|
| 883 |
+
"protected_modules_preserved": valid_modules
|
| 884 |
+
and not any(
|
| 885 |
+
name.startswith(("sam_model.", "vision_model."))
|
| 886 |
+
or name.endswith(".mlp.gate")
|
| 887 |
+
for name in quantized_modules
|
| 888 |
+
),
|
| 889 |
+
}
|
| 890 |
+
return {
|
| 891 |
+
"passed": all(checks.values()),
|
| 892 |
+
"checks": checks,
|
| 893 |
+
"sliding_window": window,
|
| 894 |
+
"source_model": summary.get("source_model"),
|
| 895 |
+
"source_revision": summary.get("source_revision"),
|
| 896 |
}
|
|
|
|
| 897 |
|
| 898 |
|
| 899 |
def _number(payload: dict, key: str) -> float | None:
|
| 900 |
+
return _finite_number(payload.get(key))
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
def validate_release_thresholds(thresholds: dict | None) -> dict[str, float | int]:
|
| 904 |
+
if thresholds is not None and not isinstance(thresholds, dict):
|
| 905 |
+
raise TypeError("thresholds must be a dictionary or None")
|
| 906 |
+
unexpected = sorted(set(thresholds or {}) - set(DEFAULT_THRESHOLDS))
|
| 907 |
+
if unexpected:
|
| 908 |
+
raise ValueError("Unknown release thresholds: " + ", ".join(unexpected))
|
| 909 |
+
limits = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
|
| 910 |
+
for name, value in limits.items():
|
| 911 |
+
if name == "min_rswa_tokens":
|
| 912 |
+
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
| 913 |
+
raise ValueError("min_rswa_tokens must be a positive integer")
|
| 914 |
+
continue
|
| 915 |
+
number = _finite_number(value)
|
| 916 |
+
if number is None or number < 0:
|
| 917 |
+
raise ValueError(f"{name} must be a finite non-negative number")
|
| 918 |
+
limits[name] = number
|
| 919 |
+
if limits["max_rswa_repetition_rate"] > 1:
|
| 920 |
+
raise ValueError("max_rswa_repetition_rate must be in [0, 1]")
|
| 921 |
+
return limits
|
| 922 |
|
| 923 |
|
| 924 |
def _gate(name: str, actual: Any, limit: Any, passed: bool, detail: str) -> dict:
|
| 925 |
+
return {
|
| 926 |
+
"name": name,
|
| 927 |
+
"passed": bool(passed),
|
| 928 |
+
"actual": actual,
|
| 929 |
+
"limit": limit,
|
| 930 |
+
"detail": detail,
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
|
| 934 |
+
def _positive_integer(payload: dict, key: str, *, minimum: int = 1) -> int | None:
|
| 935 |
+
value = payload.get(key)
|
| 936 |
+
if not isinstance(value, int) or isinstance(value, bool) or value < minimum:
|
| 937 |
+
return None
|
| 938 |
+
return value
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
def _accuracy_files(payload: dict) -> set[str] | None:
|
| 942 |
+
per_file = payload.get("per_file")
|
| 943 |
+
if not isinstance(per_file, list) or not per_file:
|
| 944 |
+
return None
|
| 945 |
+
names = [
|
| 946 |
+
item.get("file")
|
| 947 |
+
for item in per_file
|
| 948 |
+
if isinstance(item, dict) and isinstance(item.get("file"), str)
|
| 949 |
+
]
|
| 950 |
+
return set(names) if len(names) == len(per_file) == len(set(names)) else None
|
| 951 |
+
|
| 952 |
+
|
| 953 |
+
def _official_mlx_accuracy_recipe(payload: dict) -> bool:
|
| 954 |
+
settings = payload.get("generation_settings")
|
| 955 |
+
return (
|
| 956 |
+
payload.get("backend") == "mlx"
|
| 957 |
+
and payload.get("prompt") == "<image>document parsing."
|
| 958 |
+
and payload.get("profile") == "accurate"
|
| 959 |
+
and _positive_integer(payload, "max_tokens") is not None
|
| 960 |
+
and isinstance(settings, dict)
|
| 961 |
+
and _number(settings, "temperature") == 0.0
|
| 962 |
+
and _number(settings, "top_p") == 1.0
|
| 963 |
+
and _number(settings, "repetition_penalty") == 1.0
|
| 964 |
+
and _positive_integer(settings, "no_repeat_ngram_size") == 35
|
| 965 |
+
and _positive_integer(settings, "ngram_window") == 128
|
| 966 |
+
)
|
| 967 |
+
|
| 968 |
+
|
| 969 |
+
def _performance_runs_complete(payload: dict) -> bool:
|
| 970 |
+
num_runs = _positive_integer(payload, "num_runs", minimum=3)
|
| 971 |
+
max_tokens = _positive_integer(payload, "max_tokens")
|
| 972 |
+
runs = payload.get("runs")
|
| 973 |
+
if (
|
| 974 |
+
num_runs is None
|
| 975 |
+
or max_tokens is None
|
| 976 |
+
or not isinstance(runs, list)
|
| 977 |
+
or len(runs) != num_runs
|
| 978 |
+
):
|
| 979 |
+
return False
|
| 980 |
+
return all(
|
| 981 |
+
isinstance(run, dict)
|
| 982 |
+
and _positive_integer(run, "run") == index
|
| 983 |
+
and _positive_integer(run, "tokens_generated") == max_tokens
|
| 984 |
+
and run.get("tokens_generated_source") == "mlx-vlm token count"
|
| 985 |
+
and (_number(run, "tokens_per_second") or 0) > 0
|
| 986 |
+
and run.get("tokens_per_second_source") == "mlx-vlm generation_tps"
|
| 987 |
+
and (_number(run, "elapsed_seconds") or 0) > 0
|
| 988 |
+
and run.get("finish_reason") == "length"
|
| 989 |
+
for index, run in enumerate(runs, start=1)
|
| 990 |
+
)
|
| 991 |
+
|
| 992 |
+
|
| 993 |
+
def _mean_matches(value: object, values: list[float]) -> bool:
|
| 994 |
+
number = _finite_number(value)
|
| 995 |
+
if number is None or not values:
|
| 996 |
+
return False
|
| 997 |
+
expected = sum(values) / len(values)
|
| 998 |
+
return math.isclose(number, expected, rel_tol=1e-12, abs_tol=1e-12)
|
| 999 |
+
|
| 1000 |
+
|
| 1001 |
+
def _accuracy_aggregates_valid(payload: dict) -> bool:
|
| 1002 |
+
"""Recompute every release-relevant accuracy aggregate from per-file rows."""
|
| 1003 |
+
per_file = payload.get("per_file")
|
| 1004 |
+
if not isinstance(per_file, list) or not per_file:
|
| 1005 |
+
return False
|
| 1006 |
+
cer_values = []
|
| 1007 |
+
digit_values = []
|
| 1008 |
+
cjk_values = []
|
| 1009 |
+
table_values = []
|
| 1010 |
+
repetition_values = []
|
| 1011 |
+
elapsed_values = []
|
| 1012 |
+
for item in per_file:
|
| 1013 |
+
if not isinstance(item, dict):
|
| 1014 |
+
return False
|
| 1015 |
+
cer = _number(item, "cer")
|
| 1016 |
+
digit_cer = _number(item, "digit_cer")
|
| 1017 |
+
cjk_cer = _number(item, "cjk_cer")
|
| 1018 |
+
repetition = _number(item, "repetition_rate")
|
| 1019 |
+
elapsed = _number(item, "elapsed_seconds")
|
| 1020 |
+
digit_count = item.get("ref_digit_count")
|
| 1021 |
+
cjk_count = item.get("ref_cjk_count")
|
| 1022 |
+
table_score = item.get("table_score")
|
| 1023 |
+
if (
|
| 1024 |
+
cer is None
|
| 1025 |
+
or cer < 0
|
| 1026 |
+
or digit_cer is None
|
| 1027 |
+
or digit_cer < 0
|
| 1028 |
+
or cjk_cer is None
|
| 1029 |
+
or cjk_cer < 0
|
| 1030 |
+
or repetition is None
|
| 1031 |
+
or not 0 <= repetition <= 1
|
| 1032 |
+
or elapsed is None
|
| 1033 |
+
or elapsed < 0
|
| 1034 |
+
or not isinstance(digit_count, int)
|
| 1035 |
+
or isinstance(digit_count, bool)
|
| 1036 |
+
or digit_count < 0
|
| 1037 |
+
or not isinstance(cjk_count, int)
|
| 1038 |
+
or isinstance(cjk_count, bool)
|
| 1039 |
+
or cjk_count < 0
|
| 1040 |
+
):
|
| 1041 |
+
return False
|
| 1042 |
+
cer_values.append(cer)
|
| 1043 |
+
repetition_values.append(repetition)
|
| 1044 |
+
elapsed_values.append(elapsed)
|
| 1045 |
+
if digit_count > 0:
|
| 1046 |
+
digit_values.append(digit_cer)
|
| 1047 |
+
if cjk_count > 0:
|
| 1048 |
+
cjk_values.append(cjk_cer)
|
| 1049 |
+
if table_score is not None:
|
| 1050 |
+
score = _finite_number(table_score)
|
| 1051 |
+
if score is None or not 0 <= score <= 1:
|
| 1052 |
+
return False
|
| 1053 |
+
table_values.append(score)
|
| 1054 |
+
|
| 1055 |
+
total_time = _number(payload, "total_time_seconds")
|
| 1056 |
+
return (
|
| 1057 |
+
payload.get("num_images") == len(per_file)
|
| 1058 |
+
and payload.get("num_samples") == len(per_file)
|
| 1059 |
+
and payload.get("num_digit_samples") == len(digit_values)
|
| 1060 |
+
and payload.get("num_cjk_samples") == len(cjk_values)
|
| 1061 |
+
and payload.get("num_table_samples") == len(table_values)
|
| 1062 |
+
and _mean_matches(payload.get("mean_cer"), cer_values)
|
| 1063 |
+
and _mean_matches(payload.get("mean_digit_cer"), digit_values)
|
| 1064 |
+
and _mean_matches(payload.get("mean_cjk_cer"), cjk_values)
|
| 1065 |
+
and _mean_matches(payload.get("mean_table_score"), table_values)
|
| 1066 |
+
and _mean_matches(payload.get("mean_repetition_rate"), repetition_values)
|
| 1067 |
+
and total_time is not None
|
| 1068 |
+
and math.isclose(
|
| 1069 |
+
total_time,
|
| 1070 |
+
sum(elapsed_values),
|
| 1071 |
+
rel_tol=1e-12,
|
| 1072 |
+
abs_tol=1e-12,
|
| 1073 |
+
)
|
| 1074 |
+
)
|
| 1075 |
+
|
| 1076 |
+
|
| 1077 |
+
def _performance_aggregates_valid(payload: dict) -> bool:
|
| 1078 |
+
if not _performance_runs_complete(payload):
|
| 1079 |
+
return False
|
| 1080 |
+
runs = payload["runs"]
|
| 1081 |
+
tps_values = [_number(run, "tokens_per_second") for run in runs]
|
| 1082 |
+
elapsed_values = [_number(run, "elapsed_seconds") for run in runs]
|
| 1083 |
+
memory_values = [_number(run, "peak_memory_mb") for run in runs]
|
| 1084 |
+
if (
|
| 1085 |
+
any(value is None or value <= 0 for value in tps_values)
|
| 1086 |
+
or any(value is None or value <= 0 for value in elapsed_values)
|
| 1087 |
+
or any(value is None or value < 0 for value in memory_values)
|
| 1088 |
+
):
|
| 1089 |
+
return False
|
| 1090 |
+
return (
|
| 1091 |
+
_mean_matches(payload.get("mean_tps"), tps_values)
|
| 1092 |
+
and _mean_matches(payload.get("mean_elapsed_seconds"), elapsed_values)
|
| 1093 |
+
and _mean_matches(payload.get("mean_peak_memory_mb"), memory_values)
|
| 1094 |
+
)
|
| 1095 |
+
|
| 1096 |
+
|
| 1097 |
+
def _calibration_artifact_record(path: Path) -> dict:
|
| 1098 |
+
return {
|
| 1099 |
+
"filename": path.name,
|
| 1100 |
+
"size": path.stat().st_size,
|
| 1101 |
+
"sha256": sha256_file(path),
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
|
| 1105 |
+
def validate_calibration_results(
|
| 1106 |
+
calibration: dict,
|
| 1107 |
+
*,
|
| 1108 |
+
bf16_accuracy: dict,
|
| 1109 |
+
reference_performance: dict,
|
| 1110 |
+
calibration_dataset: dict,
|
| 1111 |
+
source_revision: str,
|
| 1112 |
+
reference_revision: str,
|
| 1113 |
+
evidence: dict[str, dict],
|
| 1114 |
+
evidence_paths: dict[str, str | Path],
|
| 1115 |
+
thresholds: dict | None = None,
|
| 1116 |
+
) -> dict:
|
| 1117 |
+
"""Recompute calibration selection from content-addressed raw evidence."""
|
| 1118 |
+
limits = validate_release_thresholds(thresholds)
|
| 1119 |
+
if not re.fullmatch(r"[0-9a-f]{40}", source_revision):
|
| 1120 |
+
raise ValueError("Calibration source revision must be a lowercase commit SHA")
|
| 1121 |
+
if not re.fullmatch(r"[0-9a-f]{40}", reference_revision):
|
| 1122 |
+
raise ValueError(
|
| 1123 |
+
"Calibration reference revision must be a lowercase commit SHA"
|
| 1124 |
+
)
|
| 1125 |
+
if calibration.get("schema_version") != 3:
|
| 1126 |
+
raise ValueError("Calibration schema_version 3 is required")
|
| 1127 |
+
if calibration.get("target_pattern") != "language_model.lm_head":
|
| 1128 |
+
raise ValueError("Calibration may target only language_model.lm_head")
|
| 1129 |
+
if calibration.get("thresholds") != limits:
|
| 1130 |
+
raise ValueError("Calibration thresholds do not match release thresholds")
|
| 1131 |
+
if calibration.get("selection_policy") != (
|
| 1132 |
+
"fastest candidate passing existing quality and throughput limits"
|
| 1133 |
+
):
|
| 1134 |
+
raise ValueError("Calibration selection policy is invalid")
|
| 1135 |
+
|
| 1136 |
+
expected_input_keys = {
|
| 1137 |
+
"calibration_baseline_accuracy",
|
| 1138 |
+
"calibration_reference_performance",
|
| 1139 |
+
*CALIBRATION_RAW_EVIDENCE_NAMES,
|
| 1140 |
+
}
|
| 1141 |
+
input_artifacts = calibration.get("input_artifacts")
|
| 1142 |
+
if (
|
| 1143 |
+
not isinstance(input_artifacts, dict)
|
| 1144 |
+
or set(input_artifacts) != expected_input_keys
|
| 1145 |
+
):
|
| 1146 |
+
raise ValueError("Calibration input artifacts are incomplete")
|
| 1147 |
+
for name in expected_input_keys:
|
| 1148 |
+
path = Path(evidence_paths[name])
|
| 1149 |
+
if path.is_symlink() or not path.is_file():
|
| 1150 |
+
raise FileNotFoundError(f"Calibration input is missing: {name}")
|
| 1151 |
+
if input_artifacts.get(name) != _calibration_artifact_record(path):
|
| 1152 |
+
raise ValueError(f"Calibration input hash does not match: {name}")
|
| 1153 |
+
if (
|
| 1154 |
+
evidence.get("calibration_baseline_accuracy") != bf16_accuracy
|
| 1155 |
+
or evidence.get("calibration_reference_performance")
|
| 1156 |
+
!= reference_performance
|
| 1157 |
+
):
|
| 1158 |
+
raise ValueError("Calibration baselines do not match raw evidence")
|
| 1159 |
+
|
| 1160 |
+
if not isinstance(calibration_dataset, dict) or calibration.get(
|
| 1161 |
+
"dataset"
|
| 1162 |
+
) != calibration_dataset:
|
| 1163 |
+
raise ValueError("Calibration dataset provenance does not match")
|
| 1164 |
+
expected_files = {
|
| 1165 |
+
Path(record["path"]).name
|
| 1166 |
+
for record in calibration_dataset.get("files", [])
|
| 1167 |
+
if isinstance(record, dict)
|
| 1168 |
+
and isinstance(record.get("path"), str)
|
| 1169 |
+
and record["path"].startswith("images/")
|
| 1170 |
+
}
|
| 1171 |
+
if (
|
| 1172 |
+
not expected_files
|
| 1173 |
+
or len(expected_files) != calibration_dataset.get("num_samples")
|
| 1174 |
+
or _accuracy_files(bf16_accuracy) != expected_files
|
| 1175 |
+
or bf16_accuracy.get("num_images") != calibration_dataset.get("num_samples")
|
| 1176 |
+
or bf16_accuracy.get("num_samples") != calibration_dataset.get("num_samples")
|
| 1177 |
+
or bf16_accuracy.get("num_digit_samples")
|
| 1178 |
+
!= calibration_dataset.get("num_digit_samples")
|
| 1179 |
+
or bf16_accuracy.get("num_cjk_samples")
|
| 1180 |
+
!= calibration_dataset.get("num_cjk_samples")
|
| 1181 |
+
or bf16_accuracy.get("num_table_samples")
|
| 1182 |
+
!= calibration_dataset.get("num_table_samples")
|
| 1183 |
+
):
|
| 1184 |
+
raise ValueError("Calibration baseline does not match its dataset")
|
| 1185 |
+
|
| 1186 |
+
baseline_values = {
|
| 1187 |
+
"mean_cer": _number(bf16_accuracy, "mean_cer"),
|
| 1188 |
+
"mean_digit_cer": _number(bf16_accuracy, "mean_digit_cer"),
|
| 1189 |
+
"mean_table_score": _number(bf16_accuracy, "mean_table_score"),
|
| 1190 |
+
}
|
| 1191 |
+
reference_tps = _number(reference_performance, "mean_tps")
|
| 1192 |
+
if (
|
| 1193 |
+
any(value is None for value in baseline_values.values())
|
| 1194 |
+
or reference_tps is None
|
| 1195 |
+
or reference_tps <= 0
|
| 1196 |
+
or not _accuracy_aggregates_valid(bf16_accuracy)
|
| 1197 |
+
or not _performance_aggregates_valid(reference_performance)
|
| 1198 |
+
or bf16_accuracy.get("served_revision") != source_revision
|
| 1199 |
+
or reference_performance.get("served_revision") != reference_revision
|
| 1200 |
+
):
|
| 1201 |
+
raise ValueError("Calibration baselines are incomplete or inconsistent")
|
| 1202 |
+
|
| 1203 |
+
experiments = calibration.get("experiments")
|
| 1204 |
+
if not isinstance(experiments, list) or len(experiments) != len(
|
| 1205 |
+
CALIBRATION_EVIDENCE_KEYS
|
| 1206 |
+
):
|
| 1207 |
+
raise ValueError("Calibration must contain exactly three experiments")
|
| 1208 |
+
stored_by_precision = {}
|
| 1209 |
+
labels = set()
|
| 1210 |
+
for experiment in experiments:
|
| 1211 |
+
if not isinstance(experiment, dict):
|
| 1212 |
+
raise ValueError("Calibration experiment must be an object")
|
| 1213 |
+
precision = experiment.get("precision")
|
| 1214 |
+
label = experiment.get("label")
|
| 1215 |
+
if (
|
| 1216 |
+
precision not in CALIBRATION_EVIDENCE_KEYS
|
| 1217 |
+
or precision in stored_by_precision
|
| 1218 |
+
or not isinstance(label, str)
|
| 1219 |
+
or not label
|
| 1220 |
+
or label != label.strip()
|
| 1221 |
+
or label in labels
|
| 1222 |
+
):
|
| 1223 |
+
raise ValueError("Calibration experiment identities are invalid")
|
| 1224 |
+
stored_by_precision[precision] = experiment
|
| 1225 |
+
labels.add(label)
|
| 1226 |
+
if set(stored_by_precision) != set(CALIBRATION_EVIDENCE_KEYS):
|
| 1227 |
+
raise ValueError("Calibration precision coverage is incomplete")
|
| 1228 |
+
|
| 1229 |
+
expected_experiments = []
|
| 1230 |
+
model_paths = set()
|
| 1231 |
+
accuracy_recipe_keys = ("prompt", "max_tokens", "profile", "generation_settings")
|
| 1232 |
+
performance_recipe_keys = (
|
| 1233 |
+
"image_path",
|
| 1234 |
+
"prompt",
|
| 1235 |
+
"max_tokens",
|
| 1236 |
+
"num_warmup",
|
| 1237 |
+
"num_runs",
|
| 1238 |
+
"system",
|
| 1239 |
+
)
|
| 1240 |
+
baseline_files = _accuracy_files(bf16_accuracy)
|
| 1241 |
+
for precision, (accuracy_key, performance_key) in CALIBRATION_EVIDENCE_KEYS.items():
|
| 1242 |
+
accuracy = evidence.get(accuracy_key)
|
| 1243 |
+
performance = evidence.get(performance_key)
|
| 1244 |
+
if not isinstance(accuracy, dict) or not isinstance(performance, dict):
|
| 1245 |
+
raise ValueError(f"Calibration raw evidence is missing for {precision}")
|
| 1246 |
+
model_path = accuracy.get("model_path")
|
| 1247 |
+
if (
|
| 1248 |
+
not isinstance(model_path, str)
|
| 1249 |
+
or not model_path
|
| 1250 |
+
or performance.get("model_path") != model_path
|
| 1251 |
+
or performance.get("served_revision")
|
| 1252 |
+
!= accuracy.get("served_revision")
|
| 1253 |
+
or model_path in model_paths
|
| 1254 |
+
):
|
| 1255 |
+
raise ValueError(f"Calibration model identity is invalid for {precision}")
|
| 1256 |
+
model_paths.add(model_path)
|
| 1257 |
+
if (
|
| 1258 |
+
not _official_mlx_accuracy_recipe(accuracy)
|
| 1259 |
+
or not _accuracy_aggregates_valid(accuracy)
|
| 1260 |
+
or _accuracy_files(accuracy) != baseline_files
|
| 1261 |
+
or any(
|
| 1262 |
+
accuracy.get(key) != bf16_accuracy.get(key)
|
| 1263 |
+
for key in accuracy_recipe_keys
|
| 1264 |
+
)
|
| 1265 |
+
or any(
|
| 1266 |
+
accuracy.get(key) != bf16_accuracy.get(key)
|
| 1267 |
+
for key in (
|
| 1268 |
+
"num_images",
|
| 1269 |
+
"num_samples",
|
| 1270 |
+
"num_digit_samples",
|
| 1271 |
+
"num_cjk_samples",
|
| 1272 |
+
"num_table_samples",
|
| 1273 |
+
)
|
| 1274 |
+
)
|
| 1275 |
+
):
|
| 1276 |
+
raise ValueError(f"Calibration accuracy recipe is invalid for {precision}")
|
| 1277 |
+
if not _performance_aggregates_valid(performance) or any(
|
| 1278 |
+
performance.get(key) != reference_performance.get(key)
|
| 1279 |
+
for key in performance_recipe_keys
|
| 1280 |
+
):
|
| 1281 |
+
raise ValueError(
|
| 1282 |
+
f"Calibration performance recipe is invalid for {precision}"
|
| 1283 |
+
)
|
| 1284 |
+
|
| 1285 |
+
candidate_cer = _number(accuracy, "mean_cer")
|
| 1286 |
+
candidate_digit = _number(accuracy, "mean_digit_cer")
|
| 1287 |
+
candidate_table = _number(accuracy, "mean_table_score")
|
| 1288 |
+
candidate_tps = _number(performance, "mean_tps")
|
| 1289 |
+
if None in (candidate_cer, candidate_digit, candidate_table, candidate_tps):
|
| 1290 |
+
raise ValueError(f"Calibration metrics are incomplete for {precision}")
|
| 1291 |
+
deltas = {
|
| 1292 |
+
"cer_vs_bf16": candidate_cer - baseline_values["mean_cer"],
|
| 1293 |
+
"digit_cer_vs_bf16": (candidate_digit - baseline_values["mean_digit_cer"]),
|
| 1294 |
+
"table_degradation_vs_bf16": (
|
| 1295 |
+
baseline_values["mean_table_score"] - candidate_table
|
| 1296 |
+
),
|
| 1297 |
+
"tps_ratio_vs_reference": candidate_tps / reference_tps,
|
| 1298 |
+
}
|
| 1299 |
+
checks = {
|
| 1300 |
+
"cer": deltas["cer_vs_bf16"] <= limits["max_cer_delta_vs_bf16"],
|
| 1301 |
+
"digit_cer": deltas["digit_cer_vs_bf16"]
|
| 1302 |
+
<= limits["max_digit_cer_delta_vs_bf16"],
|
| 1303 |
+
"table_score": deltas["table_degradation_vs_bf16"]
|
| 1304 |
+
<= limits["max_table_score_degradation_vs_bf16"],
|
| 1305 |
+
"throughput": deltas["tps_ratio_vs_reference"]
|
| 1306 |
+
>= limits["min_tps_ratio_vs_reference"],
|
| 1307 |
+
}
|
| 1308 |
+
expected = {
|
| 1309 |
+
"label": stored_by_precision[precision]["label"],
|
| 1310 |
+
"precision": precision,
|
| 1311 |
+
"passed": all(checks.values()),
|
| 1312 |
+
"checks": checks,
|
| 1313 |
+
"metrics": {
|
| 1314 |
+
"mean_cer": candidate_cer,
|
| 1315 |
+
"mean_digit_cer": candidate_digit,
|
| 1316 |
+
"mean_table_score": candidate_table,
|
| 1317 |
+
"mean_tps": candidate_tps,
|
| 1318 |
+
},
|
| 1319 |
+
"deltas": deltas,
|
| 1320 |
+
}
|
| 1321 |
+
if stored_by_precision[precision] != expected:
|
| 1322 |
+
raise ValueError(f"Calibration claims do not recompute for {precision}")
|
| 1323 |
+
expected_experiments.append(expected)
|
| 1324 |
+
|
| 1325 |
+
passing = [
|
| 1326 |
+
experiment for experiment in expected_experiments if experiment["passed"]
|
| 1327 |
+
]
|
| 1328 |
+
if not passing:
|
| 1329 |
+
raise ValueError("No calibration experiment passes every release limit")
|
| 1330 |
+
selected_experiment = max(
|
| 1331 |
+
passing,
|
| 1332 |
+
key=lambda experiment: experiment["metrics"]["mean_tps"],
|
| 1333 |
+
)
|
| 1334 |
+
selected = {
|
| 1335 |
+
"label": selected_experiment["label"],
|
| 1336 |
+
"precision": selected_experiment["precision"],
|
| 1337 |
+
}
|
| 1338 |
+
if calibration.get("selected") != selected:
|
| 1339 |
+
raise ValueError("Calibration did not select the fastest passing experiment")
|
| 1340 |
+
if calibration.get("precision_overrides") != {
|
| 1341 |
+
"language_model.lm_head": selected["precision"]
|
| 1342 |
+
}:
|
| 1343 |
+
raise ValueError("Calibration precision override does not match selection")
|
| 1344 |
+
return {
|
| 1345 |
+
"selected": selected,
|
| 1346 |
+
"models": sorted(model_paths),
|
| 1347 |
+
"dataset_digest": calibration_dataset.get("content_sha256"),
|
| 1348 |
+
"input_artifacts": input_artifacts,
|
| 1349 |
+
}
|
| 1350 |
|
| 1351 |
|
| 1352 |
def evaluate_release_gates(
|
|
|
|
| 1354 |
candidate_weights: dict,
|
| 1355 |
reference_weights: dict,
|
| 1356 |
dataset: dict,
|
| 1357 |
+
calibration_dataset: dict,
|
| 1358 |
metadata: dict,
|
| 1359 |
bf16_accuracy: dict,
|
| 1360 |
reference_accuracy: dict,
|
|
|
|
| 1362 |
reference_performance: dict,
|
| 1363 |
candidate_performance: dict,
|
| 1364 |
rswa: dict,
|
| 1365 |
+
provenance: dict,
|
| 1366 |
+
source_model_name: str,
|
| 1367 |
+
source_id: str,
|
| 1368 |
+
source_config_sha256: str,
|
| 1369 |
+
source_revision: str,
|
| 1370 |
+
reference_id: str,
|
| 1371 |
+
reference_revision: str,
|
| 1372 |
+
candidate_model_name: str,
|
| 1373 |
+
repo_id: str,
|
| 1374 |
thresholds: dict | None = None,
|
| 1375 |
) -> list[dict]:
|
| 1376 |
"""Evaluate every required release gate; missing values fail closed."""
|
| 1377 |
+
limits = validate_release_thresholds(thresholds)
|
| 1378 |
gates = []
|
| 1379 |
candidate_digest = candidate_weights.get("aggregate_sha256")
|
| 1380 |
reference_digest = reference_weights.get("aggregate_sha256")
|
| 1381 |
+
gates.append(
|
| 1382 |
+
_gate(
|
| 1383 |
+
"weights_are_distinct",
|
| 1384 |
+
candidate_digest,
|
| 1385 |
+
f"different from {reference_digest}",
|
| 1386 |
+
bool(
|
| 1387 |
+
candidate_digest
|
| 1388 |
+
and reference_digest
|
| 1389 |
+
and candidate_digest != reference_digest
|
| 1390 |
+
),
|
| 1391 |
+
"Candidate aggregate digest must differ from the Sahil reference",
|
| 1392 |
+
)
|
| 1393 |
+
)
|
| 1394 |
candidate_size = _number(candidate_weights, "total_size_gb")
|
| 1395 |
+
gates.append(
|
| 1396 |
+
_gate(
|
| 1397 |
+
"weight_size_gb",
|
| 1398 |
+
candidate_size,
|
| 1399 |
+
limits["max_weight_size_gb"],
|
| 1400 |
+
candidate_size is not None
|
| 1401 |
+
and candidate_size <= limits["max_weight_size_gb"],
|
| 1402 |
+
"Candidate Safetensors size",
|
| 1403 |
+
)
|
| 1404 |
+
)
|
| 1405 |
+
gates.append(
|
| 1406 |
+
_gate(
|
| 1407 |
+
"native_model_metadata",
|
| 1408 |
+
metadata.get("checks"),
|
| 1409 |
+
True,
|
| 1410 |
+
metadata.get("passed") is True,
|
| 1411 |
+
"Native Unlimited-OCR, MXFP8, and R-SWA metadata",
|
| 1412 |
+
)
|
| 1413 |
+
)
|
| 1414 |
+
candidate_source = {
|
| 1415 |
+
"model": metadata.get("source_model"),
|
| 1416 |
+
"revision": metadata.get("source_revision"),
|
| 1417 |
+
}
|
| 1418 |
+
gates.append(
|
| 1419 |
+
_gate(
|
| 1420 |
+
"candidate_source_provenance",
|
| 1421 |
+
candidate_source,
|
| 1422 |
+
{
|
| 1423 |
+
"model": [source_id, source_model_name],
|
| 1424 |
+
"revision": source_revision,
|
| 1425 |
+
},
|
| 1426 |
+
candidate_source["model"] in {source_id, source_model_name}
|
| 1427 |
+
and candidate_source["revision"] == source_revision,
|
| 1428 |
+
"Converted metadata must bind the exact BF16 source commit",
|
| 1429 |
+
)
|
| 1430 |
+
)
|
| 1431 |
|
| 1432 |
coverage = {
|
| 1433 |
"samples": dataset.get("num_samples", 0),
|
|
|
|
| 1435 |
"cjk": dataset.get("num_cjk_samples", 0),
|
| 1436 |
"table": dataset.get("num_table_samples", 0),
|
| 1437 |
}
|
| 1438 |
+
gates.append(
|
| 1439 |
+
_gate(
|
| 1440 |
+
"evaluation_coverage",
|
| 1441 |
+
coverage,
|
| 1442 |
+
"all counts > 0",
|
| 1443 |
+
all(
|
| 1444 |
+
isinstance(value, int) and not isinstance(value, bool) and value > 0
|
| 1445 |
+
for value in coverage.values()
|
| 1446 |
+
),
|
| 1447 |
+
"Dataset must cover ordinary text, digits, CJK, and tables",
|
| 1448 |
+
)
|
| 1449 |
+
)
|
| 1450 |
+
separation = dataset_separation(calibration_dataset, dataset)
|
| 1451 |
+
gates.append(
|
| 1452 |
+
_gate(
|
| 1453 |
+
"held_out_evaluation_dataset",
|
| 1454 |
+
separation,
|
| 1455 |
+
"distinct dataset digests and no shared image or ground-truth hashes",
|
| 1456 |
+
separation["passed"],
|
| 1457 |
+
"Final quality evidence must not reuse model-selection samples",
|
| 1458 |
+
)
|
| 1459 |
+
)
|
| 1460 |
+
expected_files = {
|
| 1461 |
+
Path(item["path"]).name
|
| 1462 |
+
for item in dataset.get("files", [])
|
| 1463 |
+
if isinstance(item, dict)
|
| 1464 |
+
and isinstance(item.get("path"), str)
|
| 1465 |
+
and item["path"].startswith("images/")
|
| 1466 |
+
}
|
| 1467 |
+
accuracy_payloads = (
|
| 1468 |
+
bf16_accuracy,
|
| 1469 |
+
reference_accuracy,
|
| 1470 |
+
candidate_accuracy,
|
| 1471 |
+
)
|
| 1472 |
+
gates.append(
|
| 1473 |
+
_gate(
|
| 1474 |
+
"accuracy_aggregates_recomputed",
|
| 1475 |
+
[_accuracy_aggregates_valid(payload) for payload in accuracy_payloads],
|
| 1476 |
+
[True, True, True],
|
| 1477 |
+
all(_accuracy_aggregates_valid(payload) for payload in accuracy_payloads),
|
| 1478 |
+
"Every reported accuracy aggregate must recompute from per-file rows",
|
| 1479 |
+
)
|
| 1480 |
+
)
|
| 1481 |
+
sample_counts = [payload.get("num_samples") for payload in accuracy_payloads]
|
| 1482 |
+
evaluated_files = [_accuracy_files(payload) for payload in accuracy_payloads]
|
| 1483 |
+
gates.append(
|
| 1484 |
+
_gate(
|
| 1485 |
+
"same_evaluation_samples",
|
| 1486 |
+
{
|
| 1487 |
+
"counts": sample_counts,
|
| 1488 |
+
"file_counts": [len(files or set()) for files in evaluated_files],
|
| 1489 |
+
},
|
| 1490 |
+
{"count": dataset.get("num_samples"), "files": sorted(expected_files)},
|
| 1491 |
+
bool(expected_files)
|
| 1492 |
+
and len(expected_files) == dataset.get("num_samples")
|
| 1493 |
+
and all(count == dataset.get("num_samples") for count in sample_counts)
|
| 1494 |
+
and all(
|
| 1495 |
+
payload.get("num_images") == dataset.get("num_samples")
|
| 1496 |
+
for payload in accuracy_payloads
|
| 1497 |
+
)
|
| 1498 |
+
and all(files == expected_files for files in evaluated_files),
|
| 1499 |
+
"All three checkpoints must run every identical evaluation file",
|
| 1500 |
+
)
|
| 1501 |
+
)
|
| 1502 |
+
model_identities = {
|
| 1503 |
+
"bf16": bf16_accuracy.get("model_path"),
|
| 1504 |
+
"reference": reference_accuracy.get("model_path"),
|
| 1505 |
+
"candidate": candidate_accuracy.get("model_path"),
|
| 1506 |
+
"reference_performance": reference_performance.get("model_path"),
|
| 1507 |
+
"candidate_performance": candidate_performance.get("model_path"),
|
| 1508 |
+
"rswa": rswa.get("model_path"),
|
| 1509 |
+
}
|
| 1510 |
+
gates.append(
|
| 1511 |
+
_gate(
|
| 1512 |
+
"model_identities",
|
| 1513 |
+
model_identities,
|
| 1514 |
+
{
|
| 1515 |
+
"bf16": source_model_name,
|
| 1516 |
+
"reference": reference_id,
|
| 1517 |
+
"candidate": candidate_model_name,
|
| 1518 |
+
},
|
| 1519 |
+
model_identities
|
| 1520 |
+
== {
|
| 1521 |
+
"bf16": source_model_name,
|
| 1522 |
+
"reference": reference_id,
|
| 1523 |
+
"candidate": candidate_model_name,
|
| 1524 |
+
"reference_performance": reference_id,
|
| 1525 |
+
"candidate_performance": candidate_model_name,
|
| 1526 |
+
"rswa": candidate_model_name,
|
| 1527 |
+
},
|
| 1528 |
+
"Accuracy, performance, and R-SWA evidence must identify exact models",
|
| 1529 |
+
)
|
| 1530 |
+
)
|
| 1531 |
+
model_revisions = {
|
| 1532 |
+
"bf16_accuracy": bf16_accuracy.get("served_revision"),
|
| 1533 |
+
"reference_accuracy": reference_accuracy.get("served_revision"),
|
| 1534 |
+
"candidate_accuracy": candidate_accuracy.get("served_revision"),
|
| 1535 |
+
"reference_performance": reference_performance.get("served_revision"),
|
| 1536 |
+
"candidate_performance": candidate_performance.get("served_revision"),
|
| 1537 |
+
}
|
| 1538 |
+
expected_revisions = {
|
| 1539 |
+
"bf16_accuracy": source_revision,
|
| 1540 |
+
"reference_accuracy": reference_revision,
|
| 1541 |
+
"candidate_accuracy": None,
|
| 1542 |
+
"reference_performance": reference_revision,
|
| 1543 |
+
"candidate_performance": None,
|
| 1544 |
+
}
|
| 1545 |
+
gates.append(
|
| 1546 |
+
_gate(
|
| 1547 |
+
"immutable_model_revisions",
|
| 1548 |
+
model_revisions,
|
| 1549 |
+
expected_revisions,
|
| 1550 |
+
bool(re.fullmatch(r"[0-9a-f]{40}", source_revision))
|
| 1551 |
+
and bool(re.fullmatch(r"[0-9a-f]{40}", reference_revision))
|
| 1552 |
+
and model_revisions == expected_revisions,
|
| 1553 |
+
"Remote source and reference evidence must name exact Hub commits",
|
| 1554 |
+
)
|
| 1555 |
+
)
|
| 1556 |
+
accuracy_recipe_keys = ("prompt", "max_tokens", "profile", "generation_settings")
|
| 1557 |
+
gates.append(
|
| 1558 |
+
_gate(
|
| 1559 |
+
"same_accuracy_recipe",
|
| 1560 |
+
{
|
| 1561 |
+
key: [payload.get(key) for payload in accuracy_payloads]
|
| 1562 |
+
for key in accuracy_recipe_keys
|
| 1563 |
+
},
|
| 1564 |
+
"identical official MLX OCR recipe",
|
| 1565 |
+
all(_official_mlx_accuracy_recipe(payload) for payload in accuracy_payloads)
|
| 1566 |
+
and all(
|
| 1567 |
+
payload.get(key) == bf16_accuracy.get(key)
|
| 1568 |
+
for payload in accuracy_payloads[1:]
|
| 1569 |
+
for key in accuracy_recipe_keys
|
| 1570 |
+
),
|
| 1571 |
+
"Accuracy runs must use the same deterministic OCR generation settings",
|
| 1572 |
+
)
|
| 1573 |
+
)
|
| 1574 |
|
| 1575 |
bf16_cer = _number(bf16_accuracy, "mean_cer")
|
| 1576 |
reference_cer = _number(reference_accuracy, "mean_cer")
|
| 1577 |
candidate_cer = _number(candidate_accuracy, "mean_cer")
|
| 1578 |
+
delta_bf16 = (
|
| 1579 |
+
candidate_cer - bf16_cer
|
| 1580 |
+
if candidate_cer is not None and bf16_cer is not None
|
| 1581 |
+
else None
|
| 1582 |
+
)
|
| 1583 |
+
delta_reference = (
|
| 1584 |
+
candidate_cer - reference_cer
|
| 1585 |
+
if candidate_cer is not None and reference_cer is not None
|
| 1586 |
+
else None
|
| 1587 |
+
)
|
| 1588 |
+
gates.append(
|
| 1589 |
+
_gate(
|
| 1590 |
+
"candidate_cer_vs_bf16",
|
| 1591 |
+
delta_bf16,
|
| 1592 |
+
limits["max_cer_delta_vs_bf16"],
|
| 1593 |
+
delta_bf16 is not None and delta_bf16 <= limits["max_cer_delta_vs_bf16"],
|
| 1594 |
+
"Candidate minus BF16 absolute mean CER",
|
| 1595 |
+
)
|
| 1596 |
+
)
|
| 1597 |
+
gates.append(
|
| 1598 |
+
_gate(
|
| 1599 |
+
"candidate_cer_vs_reference",
|
| 1600 |
+
delta_reference,
|
| 1601 |
+
limits["max_cer_delta_vs_reference"],
|
| 1602 |
+
delta_reference is not None
|
| 1603 |
+
and delta_reference <= limits["max_cer_delta_vs_reference"],
|
| 1604 |
+
"Candidate minus Sahil-reference absolute mean CER",
|
| 1605 |
+
)
|
| 1606 |
+
)
|
| 1607 |
|
| 1608 |
bf16_digit = _number(bf16_accuracy, "mean_digit_cer")
|
| 1609 |
candidate_digit = _number(candidate_accuracy, "mean_digit_cer")
|
| 1610 |
+
digit_delta = (
|
| 1611 |
+
candidate_digit - bf16_digit
|
| 1612 |
+
if candidate_digit is not None and bf16_digit is not None
|
| 1613 |
+
else None
|
| 1614 |
+
)
|
| 1615 |
+
gates.append(
|
| 1616 |
+
_gate(
|
| 1617 |
+
"candidate_digit_cer_vs_bf16",
|
| 1618 |
+
digit_delta,
|
| 1619 |
+
limits["max_digit_cer_delta_vs_bf16"],
|
| 1620 |
+
digit_delta is not None
|
| 1621 |
+
and digit_delta <= limits["max_digit_cer_delta_vs_bf16"],
|
| 1622 |
+
"Candidate minus BF16 digit CER",
|
| 1623 |
+
)
|
| 1624 |
+
)
|
| 1625 |
|
| 1626 |
bf16_table = _number(bf16_accuracy, "mean_table_score")
|
| 1627 |
candidate_table = _number(candidate_accuracy, "mean_table_score")
|
| 1628 |
+
table_degradation = (
|
| 1629 |
+
bf16_table - candidate_table
|
| 1630 |
+
if bf16_table is not None and candidate_table is not None
|
| 1631 |
+
else None
|
| 1632 |
+
)
|
| 1633 |
+
gates.append(
|
| 1634 |
+
_gate(
|
| 1635 |
+
"candidate_table_score_vs_bf16",
|
| 1636 |
+
table_degradation,
|
| 1637 |
+
limits["max_table_score_degradation_vs_bf16"],
|
| 1638 |
+
table_degradation is not None
|
| 1639 |
+
and table_degradation <= limits["max_table_score_degradation_vs_bf16"],
|
| 1640 |
+
"BF16 minus candidate mean table score",
|
| 1641 |
+
)
|
| 1642 |
+
)
|
| 1643 |
+
|
| 1644 |
+
performance_keys = (
|
| 1645 |
+
"image_path",
|
| 1646 |
+
"prompt",
|
| 1647 |
+
"max_tokens",
|
| 1648 |
+
"num_warmup",
|
| 1649 |
+
"num_runs",
|
| 1650 |
+
"system",
|
| 1651 |
+
)
|
| 1652 |
+
gates.append(
|
| 1653 |
+
_gate(
|
| 1654 |
+
"performance_aggregates_recomputed",
|
| 1655 |
+
{
|
| 1656 |
+
"reference": _performance_aggregates_valid(reference_performance),
|
| 1657 |
+
"candidate": _performance_aggregates_valid(candidate_performance),
|
| 1658 |
+
},
|
| 1659 |
+
{"reference": True, "candidate": True},
|
| 1660 |
+
_performance_aggregates_valid(reference_performance)
|
| 1661 |
+
and _performance_aggregates_valid(candidate_performance),
|
| 1662 |
+
"Performance means must recompute from complete benchmark runs",
|
| 1663 |
+
)
|
| 1664 |
+
)
|
| 1665 |
+
gates.append(
|
| 1666 |
+
_gate(
|
| 1667 |
+
"same_performance_setup",
|
| 1668 |
+
{
|
| 1669 |
+
key: [reference_performance.get(key), candidate_performance.get(key)]
|
| 1670 |
+
for key in performance_keys
|
| 1671 |
+
},
|
| 1672 |
+
"identical setup with at least three complete runs",
|
| 1673 |
+
all(
|
| 1674 |
+
reference_performance.get(key) == candidate_performance.get(key)
|
| 1675 |
+
for key in performance_keys
|
| 1676 |
+
)
|
| 1677 |
+
and reference_performance.get("prompt") == "<image>document parsing."
|
| 1678 |
+
and _positive_integer(reference_performance, "max_tokens") is not None
|
| 1679 |
+
and _positive_integer(reference_performance, "num_warmup") is not None
|
| 1680 |
+
and _performance_runs_complete(reference_performance)
|
| 1681 |
+
and _performance_runs_complete(candidate_performance)
|
| 1682 |
+
and isinstance(reference_performance.get("system"), dict)
|
| 1683 |
+
and bool(reference_performance["system"]),
|
| 1684 |
+
"Performance comparisons must use the same host and benchmark recipe",
|
| 1685 |
+
)
|
| 1686 |
+
)
|
| 1687 |
|
| 1688 |
reference_tps = _number(reference_performance, "mean_tps")
|
| 1689 |
candidate_tps = _number(candidate_performance, "mean_tps")
|
| 1690 |
+
tps_ratio = (
|
| 1691 |
+
candidate_tps / reference_tps
|
| 1692 |
+
if candidate_tps is not None and reference_tps and reference_tps > 0
|
| 1693 |
+
else None
|
| 1694 |
+
)
|
| 1695 |
+
gates.append(
|
| 1696 |
+
_gate(
|
| 1697 |
+
"candidate_tps_vs_reference",
|
| 1698 |
+
tps_ratio,
|
| 1699 |
+
limits["min_tps_ratio_vs_reference"],
|
| 1700 |
+
tps_ratio is not None and tps_ratio >= limits["min_tps_ratio_vs_reference"],
|
| 1701 |
+
"Candidate decode throughput divided by Sahil-reference throughput",
|
| 1702 |
+
)
|
| 1703 |
+
)
|
| 1704 |
+
rswa_results = rswa.get("test_results")
|
| 1705 |
+
long_results = (
|
| 1706 |
+
[
|
| 1707 |
+
result
|
| 1708 |
+
for result in rswa_results
|
| 1709 |
+
if isinstance(rswa_results, list)
|
| 1710 |
+
and isinstance(result, dict)
|
| 1711 |
+
and _positive_integer(result, "max_tokens") == limits["min_rswa_tokens"]
|
| 1712 |
+
]
|
| 1713 |
+
if isinstance(rswa_results, list)
|
| 1714 |
+
else []
|
| 1715 |
+
)
|
| 1716 |
+
long_result = long_results[0] if len(long_results) == 1 else {}
|
| 1717 |
+
repetition_rate = _number(long_result, "repetition_rate")
|
| 1718 |
+
forced_tokens = _positive_integer(rswa, "force_min_tokens")
|
| 1719 |
+
generated_tokens = _positive_integer(long_result, "tokens_generated")
|
| 1720 |
+
pass_conditions = rswa.get("pass_conditions")
|
| 1721 |
+
rswa_generation = rswa.get("generation_settings")
|
| 1722 |
+
gates.append(
|
| 1723 |
+
_gate(
|
| 1724 |
+
"rswa_8k_bounded",
|
| 1725 |
+
{
|
| 1726 |
+
"pass_conditions": pass_conditions,
|
| 1727 |
+
"tokens": long_result.get("tokens_generated"),
|
| 1728 |
+
"repetition_rate": repetition_rate,
|
| 1729 |
+
},
|
| 1730 |
+
{
|
| 1731 |
+
"min_tokens": limits["min_rswa_tokens"],
|
| 1732 |
+
"max_repetition_rate": limits["max_rswa_repetition_rate"],
|
| 1733 |
+
},
|
| 1734 |
+
rswa.get("passed") is True
|
| 1735 |
+
and isinstance(pass_conditions, dict)
|
| 1736 |
+
and pass_conditions.get("cache_bounded") is True
|
| 1737 |
+
and pass_conditions.get("tps_stable") is True
|
| 1738 |
+
and pass_conditions.get("8k_test_passed") is True
|
| 1739 |
+
and rswa.get("prompt") == "<image>document parsing."
|
| 1740 |
+
and isinstance(rswa_generation, dict)
|
| 1741 |
+
and _number(rswa_generation, "temperature") == 0.0
|
| 1742 |
+
and _positive_integer(rswa_generation, "no_repeat_ngram_size") == 35
|
| 1743 |
+
and _positive_integer(rswa_generation, "ngram_window") == 128
|
| 1744 |
+
and forced_tokens is not None
|
| 1745 |
+
and forced_tokens >= limits["min_rswa_tokens"]
|
| 1746 |
+
and len(long_results) == 1
|
| 1747 |
+
and long_result.get("status") == "success"
|
| 1748 |
+
and long_result.get("tokens_generated_reliable") is True
|
| 1749 |
+
and generated_tokens is not None
|
| 1750 |
+
and generated_tokens >= limits["min_rswa_tokens"]
|
| 1751 |
+
and repetition_rate is not None
|
| 1752 |
+
and 0 <= repetition_rate <= limits["max_rswa_repetition_rate"],
|
| 1753 |
+
"8K generation must have bounded cache, stable throughput, and repetition",
|
| 1754 |
+
)
|
| 1755 |
+
)
|
| 1756 |
+
provenance_dataset = provenance.get("evaluation_dataset")
|
| 1757 |
+
provenance_calibration_dataset = provenance.get("calibration_dataset")
|
| 1758 |
+
source_config = provenance.get("source_config")
|
| 1759 |
+
gates.append(
|
| 1760 |
+
_gate(
|
| 1761 |
+
"provenance_matches_release",
|
| 1762 |
+
{
|
| 1763 |
+
"source": provenance.get("source_model"),
|
| 1764 |
+
"reference": provenance.get("reference_model"),
|
| 1765 |
+
"target": provenance.get("target_repo"),
|
| 1766 |
+
"source_config": source_config,
|
| 1767 |
+
"source_revision": provenance.get("source_revision"),
|
| 1768 |
+
"reference_revision": provenance.get("reference_revision"),
|
| 1769 |
+
"dataset_digest": (
|
| 1770 |
+
provenance_dataset.get("content_sha256")
|
| 1771 |
+
if isinstance(provenance_dataset, dict)
|
| 1772 |
+
else None
|
| 1773 |
+
),
|
| 1774 |
+
"calibration_dataset_digest": (
|
| 1775 |
+
provenance_calibration_dataset.get("content_sha256")
|
| 1776 |
+
if isinstance(provenance_calibration_dataset, dict)
|
| 1777 |
+
else None
|
| 1778 |
+
),
|
| 1779 |
+
},
|
| 1780 |
+
{
|
| 1781 |
+
"source": source_id,
|
| 1782 |
+
"reference": reference_id,
|
| 1783 |
+
"target": repo_id,
|
| 1784 |
+
"source_config_sha256": source_config_sha256,
|
| 1785 |
+
"source_revision": source_revision,
|
| 1786 |
+
"reference_revision": reference_revision,
|
| 1787 |
+
"dataset_digest": dataset.get("content_sha256"),
|
| 1788 |
+
"calibration_dataset_digest": calibration_dataset.get(
|
| 1789 |
+
"content_sha256"
|
| 1790 |
+
),
|
| 1791 |
+
},
|
| 1792 |
+
provenance.get("source_model") == source_id
|
| 1793 |
+
and provenance.get("reference_model") == reference_id
|
| 1794 |
+
and provenance.get("target_repo") == repo_id
|
| 1795 |
+
and provenance.get("source_revision") == source_revision
|
| 1796 |
+
and provenance.get("reference_revision") == reference_revision
|
| 1797 |
+
and isinstance(source_config, dict)
|
| 1798 |
+
and source_config.get("path") == "config.json"
|
| 1799 |
+
and source_config.get("sha256") == source_config_sha256
|
| 1800 |
+
and provenance_dataset == dataset
|
| 1801 |
+
and provenance_calibration_dataset == calibration_dataset,
|
| 1802 |
+
"Provenance must bind the source, selection/evaluation datasets, and target",
|
| 1803 |
+
)
|
| 1804 |
+
)
|
| 1805 |
return gates
|
| 1806 |
|
| 1807 |
|
| 1808 |
def _metric_summary(payload: dict) -> dict:
|
| 1809 |
keys = (
|
| 1810 |
+
"model_path",
|
| 1811 |
+
"num_samples",
|
| 1812 |
+
"mean_cer",
|
| 1813 |
+
"mean_digit_cer",
|
| 1814 |
+
"mean_cjk_cer",
|
| 1815 |
+
"mean_table_score",
|
| 1816 |
+
"mean_tps",
|
| 1817 |
+
"mean_peak_memory_mb",
|
| 1818 |
+
"max_tokens",
|
| 1819 |
+
"profile",
|
| 1820 |
)
|
| 1821 |
return {key: payload.get(key) for key in keys if key in payload}
|
| 1822 |
|
|
|
|
| 1836 |
candidate_dir: str | Path,
|
| 1837 |
reference_dir: str | Path,
|
| 1838 |
source_dir: str | Path,
|
| 1839 |
+
calibration_dir: str | Path,
|
| 1840 |
eval_dir: str | Path,
|
| 1841 |
evidence_paths: dict[str, str | Path],
|
| 1842 |
repo_id: str,
|
| 1843 |
source_id: str,
|
| 1844 |
+
source_revision: str,
|
| 1845 |
reference_id: str,
|
| 1846 |
+
reference_revision: str,
|
| 1847 |
thresholds: dict | None = None,
|
| 1848 |
) -> dict:
|
| 1849 |
"""Build a complete release decision from on-disk evidence."""
|
| 1850 |
+
required = RELEASE_EVIDENCE_NAMES
|
| 1851 |
+
if not isinstance(evidence_paths, dict):
|
| 1852 |
+
raise TypeError("evidence_paths must be a dictionary")
|
| 1853 |
+
missing = sorted(required - set(evidence_paths))
|
|
|
|
|
|
|
|
|
|
| 1854 |
if missing:
|
| 1855 |
raise ValueError("Missing release evidence: " + ", ".join(missing))
|
| 1856 |
+
unexpected = sorted(set(evidence_paths) - required)
|
| 1857 |
+
if unexpected:
|
| 1858 |
+
raise ValueError("Unexpected release evidence: " + ", ".join(unexpected))
|
| 1859 |
+
evidence_files = [Path(path) for path in evidence_paths.values()]
|
| 1860 |
+
if len({path.name for path in evidence_files}) != len(evidence_files):
|
| 1861 |
+
raise ValueError("Release evidence filenames must be unique")
|
| 1862 |
+
symlink_evidence = [path.name for path in evidence_files if path.is_symlink()]
|
| 1863 |
+
if symlink_evidence:
|
| 1864 |
+
raise ValueError(
|
| 1865 |
+
"Release evidence must not be symbolic links: "
|
| 1866 |
+
+ ", ".join(symlink_evidence)
|
| 1867 |
+
)
|
| 1868 |
+
limits = validate_release_thresholds(thresholds)
|
| 1869 |
+
evidence = {name: load_json_object(path) for name, path in evidence_paths.items()}
|
| 1870 |
|
| 1871 |
candidate_weights = model_weight_manifest(candidate_dir)
|
| 1872 |
+
# Hugging Face snapshot directories use content-addressed links into the
|
| 1873 |
+
# immutable blob cache. Hash their resolved bytes, while keeping candidate
|
| 1874 |
+
# and source checkpoints link-free.
|
| 1875 |
+
reference_weights = model_weight_manifest(reference_dir, allow_symlinks=True)
|
| 1876 |
source_weights = model_weight_manifest(source_dir)
|
| 1877 |
+
calibration_dataset = dataset_manifest(calibration_dir)
|
| 1878 |
dataset = dataset_manifest(eval_dir)
|
| 1879 |
metadata = validate_candidate_metadata(candidate_dir)
|
| 1880 |
+
source_config_path = Path(source_dir) / "config.json"
|
| 1881 |
+
if source_config_path.is_symlink() or not source_config_path.is_file():
|
| 1882 |
+
raise FileNotFoundError(
|
| 1883 |
+
f"Source config must be a regular file: {source_config_path}"
|
| 1884 |
+
)
|
| 1885 |
+
source_config_sha256 = sha256_file(source_config_path)
|
| 1886 |
+
candidate_files = candidate_metadata_manifest(candidate_dir)
|
| 1887 |
+
project_files = release_files_manifest(
|
| 1888 |
+
PROJECT_ROOT,
|
| 1889 |
+
RELEASE_PROJECT_FILES,
|
| 1890 |
+
required=True,
|
| 1891 |
+
)
|
| 1892 |
gates = evaluate_release_gates(
|
| 1893 |
candidate_weights=candidate_weights,
|
| 1894 |
reference_weights=reference_weights,
|
| 1895 |
dataset=dataset,
|
| 1896 |
+
calibration_dataset=calibration_dataset,
|
| 1897 |
metadata=metadata,
|
| 1898 |
bf16_accuracy=evidence["bf16_accuracy"],
|
| 1899 |
reference_accuracy=evidence["reference_accuracy"],
|
|
|
|
| 1901 |
reference_performance=evidence["reference_performance"],
|
| 1902 |
candidate_performance=evidence["candidate_performance"],
|
| 1903 |
rswa=evidence["candidate_rswa"],
|
| 1904 |
+
provenance=evidence["provenance"],
|
| 1905 |
+
source_model_name=Path(source_dir).name,
|
| 1906 |
+
source_id=source_id,
|
| 1907 |
+
source_config_sha256=source_config_sha256,
|
| 1908 |
+
source_revision=source_revision,
|
| 1909 |
+
reference_id=reference_id,
|
| 1910 |
+
reference_revision=reference_revision,
|
| 1911 |
+
candidate_model_name=Path(candidate_dir).name,
|
| 1912 |
+
repo_id=repo_id,
|
| 1913 |
+
thresholds=limits,
|
| 1914 |
+
)
|
| 1915 |
+
calibration_validation = validate_calibration_results(
|
| 1916 |
+
evidence["calibration_results"],
|
| 1917 |
+
bf16_accuracy=evidence["calibration_baseline_accuracy"],
|
| 1918 |
+
reference_performance=evidence["calibration_reference_performance"],
|
| 1919 |
+
calibration_dataset=calibration_dataset,
|
| 1920 |
+
source_revision=source_revision,
|
| 1921 |
+
reference_revision=reference_revision,
|
| 1922 |
+
evidence=evidence,
|
| 1923 |
+
evidence_paths=evidence_paths,
|
| 1924 |
+
thresholds=limits,
|
| 1925 |
+
)
|
| 1926 |
+
gates.append(
|
| 1927 |
+
_gate(
|
| 1928 |
+
"calibration_recomputed",
|
| 1929 |
+
calibration_validation,
|
| 1930 |
+
"content-addressed inputs and fastest passing experiment",
|
| 1931 |
+
True,
|
| 1932 |
+
"Calibration claims must recompute from raw benchmark evidence",
|
| 1933 |
+
)
|
| 1934 |
+
)
|
| 1935 |
+
sensitivity = evidence["sensitivity_results"]
|
| 1936 |
+
sensitivity_baseline = sensitivity.get("baseline")
|
| 1937 |
+
calibration_baseline = evidence["calibration_baseline_accuracy"]
|
| 1938 |
+
sensitivity_metric_keys = (
|
| 1939 |
+
"num_samples",
|
| 1940 |
+
"num_digit_samples",
|
| 1941 |
+
"num_table_samples",
|
| 1942 |
+
"mean_cer",
|
| 1943 |
+
"mean_digit_cer",
|
| 1944 |
+
"mean_table_score",
|
| 1945 |
+
)
|
| 1946 |
+
sensitivity_matches_calibration = (
|
| 1947 |
+
isinstance(sensitivity_baseline, dict)
|
| 1948 |
+
and sensitivity.get("dataset") == calibration_dataset
|
| 1949 |
+
and sensitivity.get("prompt") == calibration_baseline.get("prompt")
|
| 1950 |
+
and sensitivity.get("max_tokens") == calibration_baseline.get("max_tokens")
|
| 1951 |
+
and all(
|
| 1952 |
+
sensitivity_baseline.get(key) == calibration_baseline.get(key)
|
| 1953 |
+
for key in sensitivity_metric_keys
|
| 1954 |
+
)
|
| 1955 |
)
|
| 1956 |
+
gates.append(
|
| 1957 |
+
_gate(
|
| 1958 |
+
"sensitivity_matches_calibration_dataset",
|
| 1959 |
+
{
|
| 1960 |
+
"dataset_digest": (
|
| 1961 |
+
sensitivity.get("dataset", {}).get("content_sha256")
|
| 1962 |
+
if isinstance(sensitivity.get("dataset"), dict)
|
| 1963 |
+
else None
|
| 1964 |
+
),
|
| 1965 |
+
"baseline_metrics_match": sensitivity_matches_calibration,
|
| 1966 |
+
},
|
| 1967 |
+
{
|
| 1968 |
+
"dataset_digest": calibration_dataset.get("content_sha256"),
|
| 1969 |
+
"baseline_metrics_match": True,
|
| 1970 |
+
},
|
| 1971 |
+
sensitivity_matches_calibration,
|
| 1972 |
+
"Sensitivity decisions must use the recorded selection dataset and baseline",
|
| 1973 |
+
)
|
| 1974 |
+
)
|
| 1975 |
+
candidate_precision_sha = sha256_file(Path(candidate_dir) / "precision_map.json")
|
| 1976 |
+
evidence_precision_sha = sha256_file(evidence_paths["generated_precision_map"])
|
| 1977 |
+
gates.append(
|
| 1978 |
+
_gate(
|
| 1979 |
+
"candidate_precision_map_matches_evidence",
|
| 1980 |
+
candidate_precision_sha,
|
| 1981 |
+
evidence_precision_sha,
|
| 1982 |
+
candidate_precision_sha == evidence_precision_sha,
|
| 1983 |
+
"The executable candidate precision map must equal approved evidence",
|
| 1984 |
+
)
|
| 1985 |
+
)
|
| 1986 |
+
evidence_map = evidence["generated_precision_map"]
|
| 1987 |
+
map_thresholds = (
|
| 1988 |
+
evidence_map.get("_generated_from", {}).get("thresholds")
|
| 1989 |
+
if isinstance(evidence_map.get("_generated_from"), dict)
|
| 1990 |
+
else None
|
| 1991 |
+
)
|
| 1992 |
+
reproduce_kwargs: dict[str, Any] = {}
|
| 1993 |
+
if isinstance(map_thresholds, dict):
|
| 1994 |
+
if map_thresholds.get("cer_delta") is not None:
|
| 1995 |
+
reproduce_kwargs["cer_threshold"] = map_thresholds["cer_delta"]
|
| 1996 |
+
if map_thresholds.get("digit_cer_delta") is not None:
|
| 1997 |
+
reproduce_kwargs["digit_cer_threshold"] = map_thresholds[
|
| 1998 |
+
"digit_cer_delta"
|
| 1999 |
+
]
|
| 2000 |
+
if map_thresholds.get("table_score_degradation") is not None:
|
| 2001 |
+
reproduce_kwargs["table_degradation_threshold"] = map_thresholds[
|
| 2002 |
+
"table_score_degradation"
|
| 2003 |
+
]
|
| 2004 |
+
reproduced_precision_map = generate_precision_map(
|
| 2005 |
+
load_json_object(PROJECT_ROOT / "quantization/precision_map.json"),
|
| 2006 |
+
evidence["sensitivity_results"],
|
| 2007 |
+
calibration_results=evidence["calibration_results"],
|
| 2008 |
+
**reproduce_kwargs,
|
| 2009 |
+
)
|
| 2010 |
+
gates.append(
|
| 2011 |
+
_gate(
|
| 2012 |
+
"precision_map_reproducible",
|
| 2013 |
+
_json_digest(evidence["generated_precision_map"]),
|
| 2014 |
+
_json_digest(reproduced_precision_map),
|
| 2015 |
+
evidence["generated_precision_map"] == reproduced_precision_map,
|
| 2016 |
+
"Sensitivity and calibration evidence must reproduce the executable map",
|
| 2017 |
+
)
|
| 2018 |
+
)
|
| 2019 |
+
actual_gate_names = tuple(gate.get("name") for gate in gates)
|
| 2020 |
+
if actual_gate_names != MLX_RELEASE_GATE_NAMES:
|
| 2021 |
+
raise RuntimeError(
|
| 2022 |
+
"MLX release gate implementation does not match its publication contract"
|
| 2023 |
+
)
|
| 2024 |
artifact_hashes = {
|
| 2025 |
name: {
|
| 2026 |
"filename": Path(path).name,
|
|
|
|
| 2029 |
}
|
| 2030 |
for name, path in evidence_paths.items()
|
| 2031 |
}
|
|
|
|
| 2032 |
return {
|
| 2033 |
+
"schema_version": MLX_RELEASE_SCHEMA_VERSION,
|
| 2034 |
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 2035 |
"release_approved": all(gate["passed"] for gate in gates),
|
| 2036 |
"repo_id": repo_id,
|
| 2037 |
+
"source": {
|
| 2038 |
+
"id": source_id,
|
| 2039 |
+
"resolved_revision": source_revision,
|
| 2040 |
+
"weights": source_weights,
|
| 2041 |
+
},
|
| 2042 |
+
"reference": {
|
| 2043 |
+
"id": reference_id,
|
| 2044 |
+
"resolved_revision": reference_revision,
|
| 2045 |
+
"weights": reference_weights,
|
| 2046 |
+
},
|
| 2047 |
"candidate": {
|
| 2048 |
"name": Path(candidate_dir).name,
|
| 2049 |
"weights": candidate_weights,
|
| 2050 |
+
"files": candidate_files,
|
| 2051 |
"metadata": metadata,
|
| 2052 |
},
|
| 2053 |
+
"project_files": project_files,
|
| 2054 |
+
"calibration_dataset": calibration_dataset,
|
| 2055 |
+
"evaluation_dataset": dataset,
|
| 2056 |
"thresholds": limits,
|
| 2057 |
"metrics": {
|
| 2058 |
name: _metric_summary(evidence[name])
|
| 2059 |
for name in (
|
| 2060 |
+
"bf16_accuracy",
|
| 2061 |
+
"reference_accuracy",
|
| 2062 |
+
"candidate_accuracy",
|
| 2063 |
+
"reference_performance",
|
| 2064 |
+
"candidate_performance",
|
| 2065 |
)
|
| 2066 |
},
|
| 2067 |
"rswa": evidence["candidate_rswa"],
|
|
|
|
| 2087 |
parser.add_argument("--candidate-dir", required=True, type=Path)
|
| 2088 |
parser.add_argument("--reference-dir", required=True, type=Path)
|
| 2089 |
parser.add_argument("--source-dir", required=True, type=Path)
|
| 2090 |
+
parser.add_argument("--calibration-dir", required=True, type=Path)
|
| 2091 |
parser.add_argument("--eval-dir", required=True, type=Path)
|
| 2092 |
parser.add_argument("--artifacts-dir", required=True, type=Path)
|
| 2093 |
parser.add_argument("--output", required=True, type=Path)
|
| 2094 |
parser.add_argument("--repo-id", required=True)
|
| 2095 |
parser.add_argument("--source-id", default="baidu/Unlimited-OCR")
|
| 2096 |
+
parser.add_argument("--source-revision", required=True)
|
| 2097 |
+
parser.add_argument(
|
| 2098 |
+
"--reference-id", default="sahilchachra/unlimited-ocr-mxfp8-mlx"
|
| 2099 |
+
)
|
| 2100 |
+
parser.add_argument("--reference-revision", required=True)
|
| 2101 |
args = parser.parse_args()
|
| 2102 |
|
| 2103 |
evidence_paths = {
|
| 2104 |
name: args.artifacts_dir / f"{name}.json"
|
| 2105 |
+
for name in RELEASE_EVIDENCE_NAMES - CALIBRATION_RAW_EVIDENCE_NAMES
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2106 |
}
|
| 2107 |
+
calibration = load_json_object(evidence_paths["calibration_results"])
|
| 2108 |
+
evidence_paths.update(
|
| 2109 |
+
calibration_raw_evidence_paths(args.artifacts_dir, calibration)
|
| 2110 |
+
)
|
| 2111 |
manifest = build_release_manifest(
|
| 2112 |
candidate_dir=args.candidate_dir,
|
| 2113 |
reference_dir=args.reference_dir,
|
| 2114 |
source_dir=args.source_dir,
|
| 2115 |
+
calibration_dir=args.calibration_dir,
|
| 2116 |
eval_dir=args.eval_dir,
|
| 2117 |
evidence_paths=evidence_paths,
|
| 2118 |
repo_id=args.repo_id,
|
| 2119 |
source_id=args.source_id,
|
| 2120 |
+
source_revision=args.source_revision,
|
| 2121 |
reference_id=args.reference_id,
|
| 2122 |
+
reference_revision=args.reference_revision,
|
| 2123 |
)
|
| 2124 |
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 2125 |
+
args.output.write_text(
|
| 2126 |
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
| 2127 |
+
)
|
| 2128 |
for gate in manifest["gates"]:
|
| 2129 |
+
print(
|
| 2130 |
+
f"[{'PASS' if gate['passed'] else 'FAIL'}] {gate['name']}: {gate['actual']}"
|
| 2131 |
+
)
|
| 2132 |
print(f"Release approved: {manifest['release_approved']}")
|
| 2133 |
if not manifest["release_approved"]:
|
| 2134 |
raise SystemExit(1)
|
quantization/run_pipeline.py
CHANGED
|
@@ -7,6 +7,7 @@ import argparse
|
|
| 7 |
import importlib.metadata
|
| 8 |
import json
|
| 9 |
import platform
|
|
|
|
| 10 |
import subprocess
|
| 11 |
import sys
|
| 12 |
import time
|
|
@@ -20,10 +21,13 @@ sys.path.insert(0, str(PROJECT_ROOT))
|
|
| 20 |
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
| 21 |
|
| 22 |
from quantization.release_gate import ( # noqa: E402
|
|
|
|
| 23 |
dataset_manifest,
|
|
|
|
| 24 |
generate_precision_map,
|
| 25 |
load_json_object,
|
| 26 |
sha256_file,
|
|
|
|
| 27 |
)
|
| 28 |
|
| 29 |
|
|
@@ -72,6 +76,18 @@ def assert_unquantized_source(model_path: str) -> dict:
|
|
| 72 |
return {"path": config_path.name, "sha256": sha256_file(config_path)}
|
| 73 |
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
def system_provenance() -> dict:
|
| 76 |
versions = {}
|
| 77 |
for distribution in ("mlx", "mlx-vlm", "huggingface-hub", "numpy", "Pillow"):
|
|
@@ -93,7 +109,19 @@ def preflight(args: argparse.Namespace) -> bool:
|
|
| 93 |
"""Validate source, dataset, and release inputs before Metal allocation."""
|
| 94 |
try:
|
| 95 |
source_config = assert_unquantized_source(args.model_path)
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
if not args.image.is_file():
|
| 98 |
raise FileNotFoundError(f"Performance/R-SWA image not found: {args.image}")
|
| 99 |
if args.output_dir.exists() and args.step in {"all", "convert"}:
|
|
@@ -102,11 +130,14 @@ def preflight(args: argparse.Namespace) -> bool:
|
|
| 102 |
provenance = {
|
| 103 |
**system_provenance(),
|
| 104 |
"source_model": args.source_id,
|
|
|
|
| 105 |
"source_local_name": Path(args.model_path).name,
|
| 106 |
"source_config": source_config,
|
| 107 |
"reference_model": args.reference_model,
|
|
|
|
| 108 |
"target_repo": args.repo_id,
|
| 109 |
-
"
|
|
|
|
| 110 |
"smoke_image": {
|
| 111 |
"name": args.image.name,
|
| 112 |
"sha256": sha256_file(args.image),
|
|
@@ -124,8 +155,10 @@ def preflight(args: argparse.Namespace) -> bool:
|
|
| 124 |
encoding="utf-8",
|
| 125 |
)
|
| 126 |
print(f"[OK] Unquantized source: {args.model_path}")
|
| 127 |
-
print(f"[OK]
|
| 128 |
-
print(f"[OK]
|
|
|
|
|
|
|
| 129 |
return True
|
| 130 |
except Exception as exc:
|
| 131 |
print(f"[FAIL] Preflight: {exc}")
|
|
@@ -138,13 +171,171 @@ def sensitivity(args: argparse.Namespace) -> bool:
|
|
| 138 |
str(QUANT_DIR / "layer_sensitivity.py"),
|
| 139 |
"--model-path", args.model_path,
|
| 140 |
"--source-id", args.source_id,
|
| 141 |
-
"--
|
|
|
|
| 142 |
"--output", str(args.artifacts_dir / "sensitivity_results.json"),
|
| 143 |
"--max-tokens", str(args.accuracy_tokens),
|
| 144 |
]
|
| 145 |
return run_command(cmd, "Layer sensitivity analysis", dry_run=args.dry_run)
|
| 146 |
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
def precision_map(args: argparse.Namespace) -> bool:
|
| 149 |
sensitivity_path = args.artifacts_dir / "sensitivity_results.json"
|
| 150 |
output_path = args.artifacts_dir / "generated_precision_map.json"
|
|
@@ -153,14 +344,39 @@ def precision_map(args: argparse.Namespace) -> bool:
|
|
| 153 |
return True
|
| 154 |
try:
|
| 155 |
calibration_path = args.artifacts_dir / "calibration_results.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
generated = generate_precision_map(
|
| 157 |
load_json_object(BASE_PRECISION_MAP),
|
| 158 |
load_json_object(sensitivity_path),
|
| 159 |
-
calibration_results=
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
)
|
| 165 |
output_path.write_text(
|
| 166 |
json.dumps(generated, indent=2, ensure_ascii=False) + "\n",
|
|
@@ -178,14 +394,21 @@ def convert(args: argparse.Namespace) -> bool:
|
|
| 178 |
sys.executable,
|
| 179 |
str(QUANT_DIR / "mixed_precision_convert.py"),
|
| 180 |
"--model-path", args.model_path,
|
|
|
|
| 181 |
"--precision-map", str(args.artifacts_dir / "generated_precision_map.json"),
|
| 182 |
"--output-dir", str(args.output_dir),
|
| 183 |
]
|
| 184 |
return run_command(cmd, "BF16 to OCR-aware MXFP8 conversion", dry_run=args.dry_run)
|
| 185 |
|
| 186 |
|
| 187 |
-
def _accuracy_command(
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
sys.executable,
|
| 190 |
str(PROJECT_ROOT / "benchmarks" / "run_accuracy.py"),
|
| 191 |
"--model-path", model,
|
|
@@ -194,10 +417,19 @@ def _accuracy_command(model: str, output: Path, args: argparse.Namespace) -> lis
|
|
| 194 |
"--max-tokens", str(args.accuracy_tokens),
|
| 195 |
"--profile", "accurate",
|
| 196 |
]
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
sys.executable,
|
| 202 |
str(PROJECT_ROOT / "benchmarks" / "run_performance.py"),
|
| 203 |
"--model-path", model,
|
|
@@ -207,15 +439,18 @@ def _performance_command(model: str, output: Path, args: argparse.Namespace) ->
|
|
| 207 |
"--warmup", str(args.performance_warmup),
|
| 208 |
"--runs", str(args.performance_runs),
|
| 209 |
]
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
|
| 212 |
def validate(args: argparse.Namespace) -> bool:
|
| 213 |
"""Benchmark BF16, Sahil reference, candidate, then stress candidate R-SWA."""
|
| 214 |
jobs = [
|
| 215 |
-
(_accuracy_command(args.model_path, args.artifacts_dir / "bf16_accuracy.json", args), "BF16 accuracy"),
|
| 216 |
-
(_accuracy_command(args.reference_model, args.artifacts_dir / "reference_accuracy.json", args), "Sahil-reference accuracy"),
|
| 217 |
(_accuracy_command(str(args.output_dir), args.artifacts_dir / "candidate_accuracy.json", args), "Candidate accuracy"),
|
| 218 |
-
(_performance_command(args.reference_model, args.artifacts_dir / "reference_performance.json", args), "Sahil-reference performance"),
|
| 219 |
(_performance_command(str(args.output_dir), args.artifacts_dir / "candidate_performance.json", args), "Candidate performance"),
|
| 220 |
([
|
| 221 |
sys.executable,
|
|
@@ -230,13 +465,14 @@ def validate(args: argparse.Namespace) -> bool:
|
|
| 230 |
return all(run_command(cmd, description, dry_run=args.dry_run) for cmd, description in jobs)
|
| 231 |
|
| 232 |
|
| 233 |
-
def resolve_reference_dir(reference_model: str) -> Path:
|
| 234 |
local = Path(reference_model)
|
| 235 |
if local.is_dir():
|
| 236 |
return local
|
| 237 |
from huggingface_hub import snapshot_download
|
| 238 |
return Path(snapshot_download(
|
| 239 |
reference_model,
|
|
|
|
| 240 |
allow_patterns=[
|
| 241 |
"*.safetensors", "model.safetensors.index.json", "config.json",
|
| 242 |
"processor_config.json", "tokenizer*.json", "special_tokens_map.json",
|
|
@@ -250,7 +486,10 @@ def gate(args: argparse.Namespace) -> bool:
|
|
| 250 |
reference_dir = Path("<resolved-reference>")
|
| 251 |
else:
|
| 252 |
try:
|
| 253 |
-
reference_dir = resolve_reference_dir(
|
|
|
|
|
|
|
|
|
|
| 254 |
except Exception as exc:
|
| 255 |
print(f"[FAIL] Reference download: {exc}")
|
| 256 |
return False
|
|
@@ -260,12 +499,15 @@ def gate(args: argparse.Namespace) -> bool:
|
|
| 260 |
"--candidate-dir", str(args.output_dir),
|
| 261 |
"--reference-dir", str(reference_dir),
|
| 262 |
"--source-dir", args.model_path,
|
|
|
|
| 263 |
"--eval-dir", str(args.eval_dir),
|
| 264 |
"--artifacts-dir", str(args.artifacts_dir),
|
| 265 |
"--output", str(args.artifacts_dir / "release_manifest.json"),
|
| 266 |
"--repo-id", args.repo_id,
|
| 267 |
"--source-id", args.source_id,
|
|
|
|
| 268 |
"--reference-id", args.reference_model,
|
|
|
|
| 269 |
]
|
| 270 |
return run_command(cmd, "Fail-closed release gate", dry_run=args.dry_run)
|
| 271 |
|
|
@@ -288,8 +530,21 @@ def parse_args() -> argparse.Namespace:
|
|
| 288 |
parser = argparse.ArgumentParser(description="Verified OCR-aware model release pipeline")
|
| 289 |
parser.add_argument("--model-path", default=str(DEFAULT_SOURCE))
|
| 290 |
parser.add_argument("--source-id", default=DEFAULT_SOURCE_ID)
|
|
|
|
| 291 |
parser.add_argument("--reference-model", default=DEFAULT_REFERENCE)
|
| 292 |
-
parser.add_argument("--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
|
| 294 |
parser.add_argument("--artifacts-dir", type=Path, default=DEFAULT_ARTIFACTS)
|
| 295 |
parser.add_argument("--image", type=Path, default=PROJECT_ROOT / "test_data" / "test_invoice.png")
|
|
@@ -301,17 +556,33 @@ def parse_args() -> argparse.Namespace:
|
|
| 301 |
parser.add_argument("--rswa-lengths", type=int, nargs="+", default=[512, 2048, 8192])
|
| 302 |
parser.add_argument(
|
| 303 |
"--step",
|
| 304 |
-
choices=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
default="all",
|
| 306 |
)
|
| 307 |
parser.add_argument("--dry-run", action="store_true")
|
| 308 |
args = parser.parse_args()
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
for name in positive:
|
| 311 |
if getattr(args, name) < 1:
|
| 312 |
parser.error(f"--{name.replace('_', '-')} must be positive")
|
| 313 |
-
if args.performance_warmup < 0:
|
| 314 |
-
parser.error("--performance-warmup must be non-negative")
|
| 315 |
if len(args.rswa_lengths) < 3 or args.rswa_lengths != sorted(set(args.rswa_lengths)) or 8192 not in args.rswa_lengths:
|
| 316 |
parser.error("--rswa-lengths must be sorted, unique, include 8192, and contain at least three values")
|
| 317 |
return args
|
|
@@ -322,12 +593,14 @@ def main() -> None:
|
|
| 322 |
steps = {
|
| 323 |
"preflight": lambda: preflight(args),
|
| 324 |
"sensitivity": lambda: sensitivity(args),
|
|
|
|
| 325 |
"precision-map": lambda: precision_map(args),
|
| 326 |
"convert": lambda: convert(args),
|
| 327 |
"validate": lambda: validate(args),
|
| 328 |
"gate": lambda: gate(args),
|
| 329 |
"publish": lambda: publish(args),
|
| 330 |
}
|
|
|
|
| 331 |
selected = list(steps) if args.step == "all" else [args.step]
|
| 332 |
for step_name in selected:
|
| 333 |
if not steps[step_name]():
|
|
|
|
| 7 |
import importlib.metadata
|
| 8 |
import json
|
| 9 |
import platform
|
| 10 |
+
import re
|
| 11 |
import subprocess
|
| 12 |
import sys
|
| 13 |
import time
|
|
|
|
| 21 |
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
| 22 |
|
| 23 |
from quantization.release_gate import ( # noqa: E402
|
| 24 |
+
calibration_raw_evidence_paths,
|
| 25 |
dataset_manifest,
|
| 26 |
+
dataset_separation,
|
| 27 |
generate_precision_map,
|
| 28 |
load_json_object,
|
| 29 |
sha256_file,
|
| 30 |
+
validate_calibration_results,
|
| 31 |
)
|
| 32 |
|
| 33 |
|
|
|
|
| 76 |
return {"path": config_path.name, "sha256": sha256_file(config_path)}
|
| 77 |
|
| 78 |
|
| 79 |
+
def verify_huggingface_revision(repo_id: str, revision: str) -> str:
|
| 80 |
+
"""Resolve an explicitly pinned Hub revision and require an exact commit."""
|
| 81 |
+
from huggingface_hub import HfApi
|
| 82 |
+
|
| 83 |
+
resolved = HfApi().model_info(repo_id, revision=revision).sha
|
| 84 |
+
if resolved != revision:
|
| 85 |
+
raise ValueError(
|
| 86 |
+
f"Revision for {repo_id} resolved to {resolved!r}, expected {revision!r}"
|
| 87 |
+
)
|
| 88 |
+
return resolved
|
| 89 |
+
|
| 90 |
+
|
| 91 |
def system_provenance() -> dict:
|
| 92 |
versions = {}
|
| 93 |
for distribution in ("mlx", "mlx-vlm", "huggingface-hub", "numpy", "Pillow"):
|
|
|
|
| 109 |
"""Validate source, dataset, and release inputs before Metal allocation."""
|
| 110 |
try:
|
| 111 |
source_config = assert_unquantized_source(args.model_path)
|
| 112 |
+
verify_huggingface_revision(args.source_id, args.source_revision)
|
| 113 |
+
verify_huggingface_revision(
|
| 114 |
+
args.reference_model,
|
| 115 |
+
args.reference_revision,
|
| 116 |
+
)
|
| 117 |
+
calibration_dataset = dataset_manifest(args.calibration_dir)
|
| 118 |
+
evaluation_dataset = dataset_manifest(args.eval_dir)
|
| 119 |
+
separation = dataset_separation(calibration_dataset, evaluation_dataset)
|
| 120 |
+
if not separation["passed"]:
|
| 121 |
+
raise ValueError(
|
| 122 |
+
"Calibration and final evaluation datasets must be disjoint; "
|
| 123 |
+
f"details={separation}"
|
| 124 |
+
)
|
| 125 |
if not args.image.is_file():
|
| 126 |
raise FileNotFoundError(f"Performance/R-SWA image not found: {args.image}")
|
| 127 |
if args.output_dir.exists() and args.step in {"all", "convert"}:
|
|
|
|
| 130 |
provenance = {
|
| 131 |
**system_provenance(),
|
| 132 |
"source_model": args.source_id,
|
| 133 |
+
"source_revision": args.source_revision,
|
| 134 |
"source_local_name": Path(args.model_path).name,
|
| 135 |
"source_config": source_config,
|
| 136 |
"reference_model": args.reference_model,
|
| 137 |
+
"reference_revision": args.reference_revision,
|
| 138 |
"target_repo": args.repo_id,
|
| 139 |
+
"calibration_dataset": calibration_dataset,
|
| 140 |
+
"evaluation_dataset": evaluation_dataset,
|
| 141 |
"smoke_image": {
|
| 142 |
"name": args.image.name,
|
| 143 |
"sha256": sha256_file(args.image),
|
|
|
|
| 155 |
encoding="utf-8",
|
| 156 |
)
|
| 157 |
print(f"[OK] Unquantized source: {args.model_path}")
|
| 158 |
+
print(f"[OK] Calibration samples: {calibration_dataset['num_samples']}")
|
| 159 |
+
print(f"[OK] Calibration digest: {calibration_dataset['content_sha256']}")
|
| 160 |
+
print(f"[OK] Held-out evaluation samples: {evaluation_dataset['num_samples']}")
|
| 161 |
+
print(f"[OK] Evaluation digest: {evaluation_dataset['content_sha256']}")
|
| 162 |
return True
|
| 163 |
except Exception as exc:
|
| 164 |
print(f"[FAIL] Preflight: {exc}")
|
|
|
|
| 171 |
str(QUANT_DIR / "layer_sensitivity.py"),
|
| 172 |
"--model-path", args.model_path,
|
| 173 |
"--source-id", args.source_id,
|
| 174 |
+
"--source-revision", args.source_revision,
|
| 175 |
+
"--eval-dir", str(args.calibration_dir),
|
| 176 |
"--output", str(args.artifacts_dir / "sensitivity_results.json"),
|
| 177 |
"--max-tokens", str(args.accuracy_tokens),
|
| 178 |
]
|
| 179 |
return run_command(cmd, "Layer sensitivity analysis", dry_run=args.dry_run)
|
| 180 |
|
| 181 |
|
| 182 |
+
def _accuracy_on_dir(
|
| 183 |
+
model: str,
|
| 184 |
+
eval_dir: Path,
|
| 185 |
+
output: Path,
|
| 186 |
+
args: argparse.Namespace,
|
| 187 |
+
*,
|
| 188 |
+
revision: str | None = None,
|
| 189 |
+
) -> list[str]:
|
| 190 |
+
command = [
|
| 191 |
+
sys.executable,
|
| 192 |
+
str(PROJECT_ROOT / "benchmarks" / "run_accuracy.py"),
|
| 193 |
+
"--model-path", model,
|
| 194 |
+
"--eval-dir", str(eval_dir),
|
| 195 |
+
"--output", str(output),
|
| 196 |
+
"--max-tokens", str(args.accuracy_tokens),
|
| 197 |
+
"--profile", "accurate",
|
| 198 |
+
]
|
| 199 |
+
if revision is not None:
|
| 200 |
+
command.extend(["--served-revision", revision])
|
| 201 |
+
return command
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def calibrate(args: argparse.Namespace) -> bool:
|
| 205 |
+
"""Build LM-head candidates from sensitivity, measure them, select precision.
|
| 206 |
+
|
| 207 |
+
Schema-3 release requires ``calibration_results.json`` with hashed raw
|
| 208 |
+
evidence before the final precision map can be generated.
|
| 209 |
+
"""
|
| 210 |
+
if args.dry_run:
|
| 211 |
+
print("[DRY RUN] Calibrate LM head (bf16 / mxfp8 / affine8 experiments)")
|
| 212 |
+
return True
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
sensitivity_path = args.artifacts_dir / "sensitivity_results.json"
|
| 216 |
+
# Align group retention with the joint release CER/digit budgets so
|
| 217 |
+
# cumulative MXFP8 error is less likely to blow the 0.01 absolute caps.
|
| 218 |
+
interim_map = generate_precision_map(
|
| 219 |
+
load_json_object(BASE_PRECISION_MAP),
|
| 220 |
+
load_json_object(sensitivity_path),
|
| 221 |
+
calibration_results=None,
|
| 222 |
+
cer_threshold=0.01,
|
| 223 |
+
digit_cer_threshold=0.01,
|
| 224 |
+
table_degradation_threshold=0.01,
|
| 225 |
+
)
|
| 226 |
+
interim_path = args.artifacts_dir / "interim_precision_map.json"
|
| 227 |
+
interim_path.write_text(
|
| 228 |
+
json.dumps(interim_map, indent=2, ensure_ascii=False) + "\n",
|
| 229 |
+
encoding="utf-8",
|
| 230 |
+
)
|
| 231 |
+
print(f"[OK] Interim precision map (pre-calibration): {interim_path}")
|
| 232 |
+
except Exception as exc:
|
| 233 |
+
print(f"[FAIL] Interim precision map: {exc}")
|
| 234 |
+
return False
|
| 235 |
+
|
| 236 |
+
baseline_accuracy = args.artifacts_dir / "calibration_baseline_accuracy.json"
|
| 237 |
+
reference_performance = (
|
| 238 |
+
args.artifacts_dir / "calibration_reference_performance.json"
|
| 239 |
+
)
|
| 240 |
+
jobs: list[tuple[list[str], str]] = [
|
| 241 |
+
(
|
| 242 |
+
_accuracy_on_dir(
|
| 243 |
+
args.model_path,
|
| 244 |
+
args.calibration_dir,
|
| 245 |
+
baseline_accuracy,
|
| 246 |
+
args,
|
| 247 |
+
revision=args.source_revision,
|
| 248 |
+
),
|
| 249 |
+
"Calibration baseline BF16 accuracy",
|
| 250 |
+
),
|
| 251 |
+
(
|
| 252 |
+
_performance_command(
|
| 253 |
+
args.reference_model,
|
| 254 |
+
reference_performance,
|
| 255 |
+
args,
|
| 256 |
+
revision=args.reference_revision,
|
| 257 |
+
),
|
| 258 |
+
"Calibration reference performance",
|
| 259 |
+
),
|
| 260 |
+
]
|
| 261 |
+
if not all(run_command(cmd, description) for cmd, description in jobs):
|
| 262 |
+
return False
|
| 263 |
+
|
| 264 |
+
experiment_specs = (
|
| 265 |
+
("bf16-head", "bfloat16"),
|
| 266 |
+
("mxfp8-head", "mxfp8"),
|
| 267 |
+
("affine8-head", "affine8"),
|
| 268 |
+
)
|
| 269 |
+
experiment_args: list[str] = []
|
| 270 |
+
for label, precision in experiment_specs:
|
| 271 |
+
exp_map = dict(interim_map)
|
| 272 |
+
exp_map["language_model.lm_head"] = precision
|
| 273 |
+
exp_map_path = args.artifacts_dir / f"calibration_{precision}_precision_map.json"
|
| 274 |
+
exp_map_path.write_text(
|
| 275 |
+
json.dumps(exp_map, indent=2, ensure_ascii=False) + "\n",
|
| 276 |
+
encoding="utf-8",
|
| 277 |
+
)
|
| 278 |
+
exp_model_dir = (
|
| 279 |
+
args.output_dir.parent / f"{args.output_dir.name}-cal-{precision}"
|
| 280 |
+
)
|
| 281 |
+
if exp_model_dir.exists():
|
| 282 |
+
print(f"[FAIL] Calibration model directory already exists: {exp_model_dir}")
|
| 283 |
+
return False
|
| 284 |
+
convert_cmd = [
|
| 285 |
+
sys.executable,
|
| 286 |
+
str(QUANT_DIR / "mixed_precision_convert.py"),
|
| 287 |
+
"--model-path", args.model_path,
|
| 288 |
+
"--source-revision", args.source_revision,
|
| 289 |
+
"--precision-map", str(exp_map_path),
|
| 290 |
+
"--output-dir", str(exp_model_dir),
|
| 291 |
+
]
|
| 292 |
+
if not run_command(convert_cmd, f"Calibration convert ({label})"):
|
| 293 |
+
return False
|
| 294 |
+
|
| 295 |
+
# Filenames must match CALIBRATION_EVIDENCE_KEYS in release_gate.py.
|
| 296 |
+
accuracy_path = args.artifacts_dir / f"calibration_{precision}_accuracy.json"
|
| 297 |
+
performance_path = (
|
| 298 |
+
args.artifacts_dir / f"calibration_{precision}_performance.json"
|
| 299 |
+
)
|
| 300 |
+
if not run_command(
|
| 301 |
+
_accuracy_on_dir(
|
| 302 |
+
str(exp_model_dir),
|
| 303 |
+
args.calibration_dir,
|
| 304 |
+
accuracy_path,
|
| 305 |
+
args,
|
| 306 |
+
),
|
| 307 |
+
f"Calibration accuracy ({label})",
|
| 308 |
+
):
|
| 309 |
+
return False
|
| 310 |
+
if not run_command(
|
| 311 |
+
_performance_command(str(exp_model_dir), performance_path, args),
|
| 312 |
+
f"Calibration performance ({label})",
|
| 313 |
+
):
|
| 314 |
+
return False
|
| 315 |
+
experiment_args.extend(
|
| 316 |
+
[
|
| 317 |
+
"--experiment",
|
| 318 |
+
label,
|
| 319 |
+
precision,
|
| 320 |
+
str(accuracy_path),
|
| 321 |
+
str(performance_path),
|
| 322 |
+
]
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
select_cmd = [
|
| 326 |
+
sys.executable,
|
| 327 |
+
str(QUANT_DIR / "calibrate_precision.py"),
|
| 328 |
+
"--bf16-accuracy", str(baseline_accuracy),
|
| 329 |
+
"--reference-performance", str(reference_performance),
|
| 330 |
+
"--calibration-dir", str(args.calibration_dir),
|
| 331 |
+
"--source-revision", args.source_revision,
|
| 332 |
+
"--reference-revision", args.reference_revision,
|
| 333 |
+
*experiment_args,
|
| 334 |
+
"--output", str(args.artifacts_dir / "calibration_results.json"),
|
| 335 |
+
]
|
| 336 |
+
return run_command(select_cmd, "Select calibrated LM-head precision")
|
| 337 |
+
|
| 338 |
+
|
| 339 |
def precision_map(args: argparse.Namespace) -> bool:
|
| 340 |
sensitivity_path = args.artifacts_dir / "sensitivity_results.json"
|
| 341 |
output_path = args.artifacts_dir / "generated_precision_map.json"
|
|
|
|
| 344 |
return True
|
| 345 |
try:
|
| 346 |
calibration_path = args.artifacts_dir / "calibration_results.json"
|
| 347 |
+
calibration = load_json_object(calibration_path)
|
| 348 |
+
evidence_paths = {
|
| 349 |
+
"calibration_baseline_accuracy": (
|
| 350 |
+
args.artifacts_dir / "calibration_baseline_accuracy.json"
|
| 351 |
+
),
|
| 352 |
+
"calibration_reference_performance": (
|
| 353 |
+
args.artifacts_dir / "calibration_reference_performance.json"
|
| 354 |
+
),
|
| 355 |
+
**calibration_raw_evidence_paths(args.artifacts_dir, calibration),
|
| 356 |
+
}
|
| 357 |
+
evidence = {
|
| 358 |
+
name: load_json_object(path) for name, path in evidence_paths.items()
|
| 359 |
+
}
|
| 360 |
+
validate_calibration_results(
|
| 361 |
+
calibration,
|
| 362 |
+
bf16_accuracy=evidence["calibration_baseline_accuracy"],
|
| 363 |
+
reference_performance=evidence[
|
| 364 |
+
"calibration_reference_performance"
|
| 365 |
+
],
|
| 366 |
+
calibration_dataset=dataset_manifest(args.calibration_dir),
|
| 367 |
+
source_revision=args.source_revision,
|
| 368 |
+
reference_revision=args.reference_revision,
|
| 369 |
+
evidence=evidence,
|
| 370 |
+
evidence_paths=evidence_paths,
|
| 371 |
+
)
|
| 372 |
generated = generate_precision_map(
|
| 373 |
load_json_object(BASE_PRECISION_MAP),
|
| 374 |
load_json_object(sensitivity_path),
|
| 375 |
+
calibration_results=calibration,
|
| 376 |
+
# Must match the interim calibration map thresholds.
|
| 377 |
+
cer_threshold=0.01,
|
| 378 |
+
digit_cer_threshold=0.01,
|
| 379 |
+
table_degradation_threshold=0.01,
|
| 380 |
)
|
| 381 |
output_path.write_text(
|
| 382 |
json.dumps(generated, indent=2, ensure_ascii=False) + "\n",
|
|
|
|
| 394 |
sys.executable,
|
| 395 |
str(QUANT_DIR / "mixed_precision_convert.py"),
|
| 396 |
"--model-path", args.model_path,
|
| 397 |
+
"--source-revision", args.source_revision,
|
| 398 |
"--precision-map", str(args.artifacts_dir / "generated_precision_map.json"),
|
| 399 |
"--output-dir", str(args.output_dir),
|
| 400 |
]
|
| 401 |
return run_command(cmd, "BF16 to OCR-aware MXFP8 conversion", dry_run=args.dry_run)
|
| 402 |
|
| 403 |
|
| 404 |
+
def _accuracy_command(
|
| 405 |
+
model: str,
|
| 406 |
+
output: Path,
|
| 407 |
+
args: argparse.Namespace,
|
| 408 |
+
*,
|
| 409 |
+
revision: str | None = None,
|
| 410 |
+
) -> list[str]:
|
| 411 |
+
command = [
|
| 412 |
sys.executable,
|
| 413 |
str(PROJECT_ROOT / "benchmarks" / "run_accuracy.py"),
|
| 414 |
"--model-path", model,
|
|
|
|
| 417 |
"--max-tokens", str(args.accuracy_tokens),
|
| 418 |
"--profile", "accurate",
|
| 419 |
]
|
| 420 |
+
if revision is not None:
|
| 421 |
+
command.extend(["--served-revision", revision])
|
| 422 |
+
return command
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def _performance_command(
|
| 426 |
+
model: str,
|
| 427 |
+
output: Path,
|
| 428 |
+
args: argparse.Namespace,
|
| 429 |
+
*,
|
| 430 |
+
revision: str | None = None,
|
| 431 |
+
) -> list[str]:
|
| 432 |
+
command = [
|
| 433 |
sys.executable,
|
| 434 |
str(PROJECT_ROOT / "benchmarks" / "run_performance.py"),
|
| 435 |
"--model-path", model,
|
|
|
|
| 439 |
"--warmup", str(args.performance_warmup),
|
| 440 |
"--runs", str(args.performance_runs),
|
| 441 |
]
|
| 442 |
+
if revision is not None:
|
| 443 |
+
command.extend(["--served-revision", revision])
|
| 444 |
+
return command
|
| 445 |
|
| 446 |
|
| 447 |
def validate(args: argparse.Namespace) -> bool:
|
| 448 |
"""Benchmark BF16, Sahil reference, candidate, then stress candidate R-SWA."""
|
| 449 |
jobs = [
|
| 450 |
+
(_accuracy_command(args.model_path, args.artifacts_dir / "bf16_accuracy.json", args, revision=args.source_revision), "BF16 accuracy"),
|
| 451 |
+
(_accuracy_command(args.reference_model, args.artifacts_dir / "reference_accuracy.json", args, revision=args.reference_revision), "Sahil-reference accuracy"),
|
| 452 |
(_accuracy_command(str(args.output_dir), args.artifacts_dir / "candidate_accuracy.json", args), "Candidate accuracy"),
|
| 453 |
+
(_performance_command(args.reference_model, args.artifacts_dir / "reference_performance.json", args, revision=args.reference_revision), "Sahil-reference performance"),
|
| 454 |
(_performance_command(str(args.output_dir), args.artifacts_dir / "candidate_performance.json", args), "Candidate performance"),
|
| 455 |
([
|
| 456 |
sys.executable,
|
|
|
|
| 465 |
return all(run_command(cmd, description, dry_run=args.dry_run) for cmd, description in jobs)
|
| 466 |
|
| 467 |
|
| 468 |
+
def resolve_reference_dir(reference_model: str, revision: str) -> Path:
|
| 469 |
local = Path(reference_model)
|
| 470 |
if local.is_dir():
|
| 471 |
return local
|
| 472 |
from huggingface_hub import snapshot_download
|
| 473 |
return Path(snapshot_download(
|
| 474 |
reference_model,
|
| 475 |
+
revision=revision,
|
| 476 |
allow_patterns=[
|
| 477 |
"*.safetensors", "model.safetensors.index.json", "config.json",
|
| 478 |
"processor_config.json", "tokenizer*.json", "special_tokens_map.json",
|
|
|
|
| 486 |
reference_dir = Path("<resolved-reference>")
|
| 487 |
else:
|
| 488 |
try:
|
| 489 |
+
reference_dir = resolve_reference_dir(
|
| 490 |
+
args.reference_model,
|
| 491 |
+
args.reference_revision,
|
| 492 |
+
)
|
| 493 |
except Exception as exc:
|
| 494 |
print(f"[FAIL] Reference download: {exc}")
|
| 495 |
return False
|
|
|
|
| 499 |
"--candidate-dir", str(args.output_dir),
|
| 500 |
"--reference-dir", str(reference_dir),
|
| 501 |
"--source-dir", args.model_path,
|
| 502 |
+
"--calibration-dir", str(args.calibration_dir),
|
| 503 |
"--eval-dir", str(args.eval_dir),
|
| 504 |
"--artifacts-dir", str(args.artifacts_dir),
|
| 505 |
"--output", str(args.artifacts_dir / "release_manifest.json"),
|
| 506 |
"--repo-id", args.repo_id,
|
| 507 |
"--source-id", args.source_id,
|
| 508 |
+
"--source-revision", args.source_revision,
|
| 509 |
"--reference-id", args.reference_model,
|
| 510 |
+
"--reference-revision", args.reference_revision,
|
| 511 |
]
|
| 512 |
return run_command(cmd, "Fail-closed release gate", dry_run=args.dry_run)
|
| 513 |
|
|
|
|
| 530 |
parser = argparse.ArgumentParser(description="Verified OCR-aware model release pipeline")
|
| 531 |
parser.add_argument("--model-path", default=str(DEFAULT_SOURCE))
|
| 532 |
parser.add_argument("--source-id", default=DEFAULT_SOURCE_ID)
|
| 533 |
+
parser.add_argument("--source-revision", required=True)
|
| 534 |
parser.add_argument("--reference-model", default=DEFAULT_REFERENCE)
|
| 535 |
+
parser.add_argument("--reference-revision", required=True)
|
| 536 |
+
parser.add_argument(
|
| 537 |
+
"--calibration-dir",
|
| 538 |
+
required=True,
|
| 539 |
+
type=Path,
|
| 540 |
+
help="Selection-only OCR dataset used for sensitivity and head calibration",
|
| 541 |
+
)
|
| 542 |
+
parser.add_argument(
|
| 543 |
+
"--eval-dir",
|
| 544 |
+
required=True,
|
| 545 |
+
type=Path,
|
| 546 |
+
help="Disjoint held-out OCR dataset used only for final release validation",
|
| 547 |
+
)
|
| 548 |
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)
|
| 549 |
parser.add_argument("--artifacts-dir", type=Path, default=DEFAULT_ARTIFACTS)
|
| 550 |
parser.add_argument("--image", type=Path, default=PROJECT_ROOT / "test_data" / "test_invoice.png")
|
|
|
|
| 556 |
parser.add_argument("--rswa-lengths", type=int, nargs="+", default=[512, 2048, 8192])
|
| 557 |
parser.add_argument(
|
| 558 |
"--step",
|
| 559 |
+
choices=[
|
| 560 |
+
"all",
|
| 561 |
+
"preflight",
|
| 562 |
+
"sensitivity",
|
| 563 |
+
"calibrate",
|
| 564 |
+
"precision-map",
|
| 565 |
+
"convert",
|
| 566 |
+
"validate",
|
| 567 |
+
"gate",
|
| 568 |
+
"publish",
|
| 569 |
+
],
|
| 570 |
default="all",
|
| 571 |
)
|
| 572 |
parser.add_argument("--dry-run", action="store_true")
|
| 573 |
args = parser.parse_args()
|
| 574 |
+
for name in ("source_revision", "reference_revision"):
|
| 575 |
+
if not re.fullmatch(r"[0-9a-f]{40}", getattr(args, name)):
|
| 576 |
+
parser.error(f"--{name.replace('_', '-')} must be a 40-character lowercase commit SHA")
|
| 577 |
+
positive = (
|
| 578 |
+
"accuracy_tokens",
|
| 579 |
+
"performance_tokens",
|
| 580 |
+
"performance_warmup",
|
| 581 |
+
"performance_runs",
|
| 582 |
+
)
|
| 583 |
for name in positive:
|
| 584 |
if getattr(args, name) < 1:
|
| 585 |
parser.error(f"--{name.replace('_', '-')} must be positive")
|
|
|
|
|
|
|
| 586 |
if len(args.rswa_lengths) < 3 or args.rswa_lengths != sorted(set(args.rswa_lengths)) or 8192 not in args.rswa_lengths:
|
| 587 |
parser.error("--rswa-lengths must be sorted, unique, include 8192, and contain at least three values")
|
| 588 |
return args
|
|
|
|
| 593 |
steps = {
|
| 594 |
"preflight": lambda: preflight(args),
|
| 595 |
"sensitivity": lambda: sensitivity(args),
|
| 596 |
+
"calibrate": lambda: calibrate(args),
|
| 597 |
"precision-map": lambda: precision_map(args),
|
| 598 |
"convert": lambda: convert(args),
|
| 599 |
"validate": lambda: validate(args),
|
| 600 |
"gate": lambda: gate(args),
|
| 601 |
"publish": lambda: publish(args),
|
| 602 |
}
|
| 603 |
+
# Full release order: selection data → calibrated map → convert → held-out gate.
|
| 604 |
selected = list(steps) if args.step == "all" else [args.step]
|
| 605 |
for step_name in selected:
|
| 606 |
if not steps[step_name]():
|
quantization_config.json
CHANGED
|
@@ -1,26 +1,27 @@
|
|
| 1 |
{
|
| 2 |
"quantization_method": "mxfp8",
|
| 3 |
-
"description": "
|
| 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 |
-
"
|
| 16 |
},
|
| 17 |
"effective_bits_per_weight": 9.19,
|
| 18 |
"model_size_gb": 3.83,
|
| 19 |
"conversion_tool": "mlx-vlm quantizers",
|
| 20 |
"notes": [
|
| 21 |
-
"
|
| 22 |
-
"The
|
| 23 |
-
"
|
| 24 |
"Published config.json uses model_type 'unlimited-ocr' to select mlx-vlm's native R-SWA implementation"
|
| 25 |
]
|
| 26 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"quantization_method": "mxfp8",
|
| 3 |
+
"description": "Quantization layout of the reference sahilchachra checkpoint used by the default upload workflow. MXFP8 is applied to quantizable language modules, token embeddings, the LM head, and the projector; the vision encoders and non-quantizable normalization/routing weights remain in bfloat16.",
|
| 4 |
"base_model": "baidu/Unlimited-OCR",
|
| 5 |
"reference_quantization": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 6 |
"quantized_components": {
|
| 7 |
"language_model.linear": "mxfp8",
|
| 8 |
+
"language_model.moe.experts": "mxfp8",
|
| 9 |
+
"language_model.token_embeddings": "mxfp8",
|
| 10 |
+
"language_model.lm_head": "mxfp8",
|
| 11 |
+
"vision_projector": "mxfp8"
|
| 12 |
},
|
| 13 |
"preserved_bf16_components": {
|
| 14 |
"vision_encoder": "bfloat16",
|
|
|
|
|
|
|
| 15 |
"normalization_layers": "bfloat16",
|
| 16 |
+
"moe_routing_gates": "bfloat16"
|
| 17 |
},
|
| 18 |
"effective_bits_per_weight": 9.19,
|
| 19 |
"model_size_gb": 3.83,
|
| 20 |
"conversion_tool": "mlx-vlm quantizers",
|
| 21 |
"notes": [
|
| 22 |
+
"This file describes the reference weights copied by scripts/upload_model.py; that workflow does not run a new conversion",
|
| 23 |
+
"The safetensors index contains MXFP8 scale tensors for the token embeddings, LM head, and projector, but not the vision encoders",
|
| 24 |
+
"quantization/mixed_precision_convert.py can produce a different OCR-aware layout from the BF16 base model",
|
| 25 |
"Published config.json uses model_type 'unlimited-ocr' to select mlx-vlm's native R-SWA implementation"
|
| 26 |
]
|
| 27 |
}
|
quantization_summary.json
CHANGED
|
@@ -2,14 +2,15 @@
|
|
| 2 |
"method": "mxfp8",
|
| 3 |
"group_size": 32,
|
| 4 |
"bits": 8,
|
| 5 |
-
"source_model": "
|
| 6 |
-
"
|
| 7 |
-
"
|
|
|
|
| 8 |
"quantized_precision_counts": {
|
| 9 |
"affine8": 1,
|
| 10 |
-
"mxfp8":
|
| 11 |
},
|
| 12 |
-
"preserved_quantizable_module_count":
|
| 13 |
"quantized_modules": [
|
| 14 |
"language_model.lm_head",
|
| 15 |
"language_model.model.embed_tokens",
|
|
@@ -129,8 +130,7 @@
|
|
| 129 |
"language_model.model.layers.9.self_attn.k_proj",
|
| 130 |
"language_model.model.layers.9.self_attn.o_proj",
|
| 131 |
"language_model.model.layers.9.self_attn.q_proj",
|
| 132 |
-
"language_model.model.layers.9.self_attn.v_proj"
|
| 133 |
-
"projector.layers"
|
| 134 |
],
|
| 135 |
"quantized_module_precisions": {
|
| 136 |
"language_model.lm_head": "affine8",
|
|
@@ -251,7 +251,6 @@
|
|
| 251 |
"language_model.model.layers.9.self_attn.k_proj": "mxfp8",
|
| 252 |
"language_model.model.layers.9.self_attn.o_proj": "mxfp8",
|
| 253 |
"language_model.model.layers.9.self_attn.q_proj": "mxfp8",
|
| 254 |
-
"language_model.model.layers.9.self_attn.v_proj": "mxfp8"
|
| 255 |
-
"projector.layers": "mxfp8"
|
| 256 |
}
|
| 257 |
}
|
|
|
|
| 2 |
"method": "mxfp8",
|
| 3 |
"group_size": 32,
|
| 4 |
"bits": 8,
|
| 5 |
+
"source_model": "Unlimited-OCR",
|
| 6 |
+
"source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 7 |
+
"precision_map_sha256": "c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a",
|
| 8 |
+
"quantized_module_count": 119,
|
| 9 |
"quantized_precision_counts": {
|
| 10 |
"affine8": 1,
|
| 11 |
+
"mxfp8": 118
|
| 12 |
},
|
| 13 |
+
"preserved_quantizable_module_count": 146,
|
| 14 |
"quantized_modules": [
|
| 15 |
"language_model.lm_head",
|
| 16 |
"language_model.model.embed_tokens",
|
|
|
|
| 130 |
"language_model.model.layers.9.self_attn.k_proj",
|
| 131 |
"language_model.model.layers.9.self_attn.o_proj",
|
| 132 |
"language_model.model.layers.9.self_attn.q_proj",
|
| 133 |
+
"language_model.model.layers.9.self_attn.v_proj"
|
|
|
|
| 134 |
],
|
| 135 |
"quantized_module_precisions": {
|
| 136 |
"language_model.lm_head": "affine8",
|
|
|
|
| 251 |
"language_model.model.layers.9.self_attn.k_proj": "mxfp8",
|
| 252 |
"language_model.model.layers.9.self_attn.o_proj": "mxfp8",
|
| 253 |
"language_model.model.layers.9.self_attn.q_proj": "mxfp8",
|
| 254 |
+
"language_model.model.layers.9.self_attn.v_proj": "mxfp8"
|
|
|
|
| 255 |
}
|
| 256 |
}
|
release/bf16_accuracy.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"model_path": "Unlimited-OCR",
|
|
|
|
|
|
|
| 3 |
"prompt": "<image>document parsing.",
|
| 4 |
"max_tokens": 1024,
|
| 5 |
"profile": "accurate",
|
|
@@ -12,183 +14,237 @@
|
|
| 12 |
},
|
| 13 |
"num_images": 12,
|
| 14 |
"num_samples": 12,
|
| 15 |
-
"total_time_seconds":
|
| 16 |
-
"mean_cer": 0.
|
| 17 |
-
"mean_wer": 0.
|
| 18 |
"num_digit_samples": 12,
|
| 19 |
"num_cjk_samples": 3,
|
| 20 |
-
"mean_digit_cer": 0.
|
| 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 |
-
"
|
| 29 |
-
"
|
| 30 |
-
"
|
|
|
|
| 31 |
"cjk_cer": 0.0,
|
| 32 |
"repetition_rate": 0.0,
|
| 33 |
"table_score": null,
|
| 34 |
-
"elapsed_seconds":
|
| 35 |
-
"ref_length":
|
| 36 |
-
"hyp_length":
|
| 37 |
-
"ref_digit_count":
|
| 38 |
"ref_cjk_count": 0
|
| 39 |
},
|
| 40 |
{
|
| 41 |
"file": "0002.png",
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
|
|
|
| 45 |
"cjk_cer": 0.0,
|
| 46 |
"repetition_rate": 0.0,
|
| 47 |
"table_score": null,
|
| 48 |
-
"elapsed_seconds":
|
| 49 |
-
"ref_length":
|
| 50 |
-
"hyp_length":
|
| 51 |
-
"ref_digit_count":
|
| 52 |
"ref_cjk_count": 0
|
| 53 |
},
|
| 54 |
{
|
| 55 |
"file": "0003.png",
|
| 56 |
-
"
|
| 57 |
-
"
|
| 58 |
-
"
|
|
|
|
| 59 |
"cjk_cer": 0.0,
|
| 60 |
"repetition_rate": 0.0,
|
| 61 |
"table_score": null,
|
| 62 |
-
"elapsed_seconds":
|
| 63 |
-
"ref_length":
|
| 64 |
-
"hyp_length":
|
| 65 |
-
"ref_digit_count":
|
| 66 |
"ref_cjk_count": 0
|
| 67 |
},
|
| 68 |
{
|
| 69 |
"file": "0004.png",
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"
|
|
|
|
| 73 |
"cjk_cer": 0.0,
|
| 74 |
"repetition_rate": 0.0,
|
| 75 |
"table_score": null,
|
| 76 |
-
"elapsed_seconds":
|
| 77 |
-
"ref_length":
|
| 78 |
-
"hyp_length":
|
| 79 |
-
"ref_digit_count":
|
| 80 |
"ref_cjk_count": 0
|
| 81 |
},
|
| 82 |
{
|
| 83 |
"file": "0005.png",
|
| 84 |
-
"
|
|
|
|
| 85 |
"wer": 1.5,
|
| 86 |
-
"digit_cer": 0.
|
| 87 |
"cjk_cer": 0.0,
|
| 88 |
"repetition_rate": 0.0,
|
| 89 |
"table_score": 1.0,
|
| 90 |
-
"elapsed_seconds":
|
| 91 |
-
"ref_length":
|
| 92 |
-
"hyp_length":
|
| 93 |
-
"ref_digit_count":
|
| 94 |
"ref_cjk_count": 0
|
| 95 |
},
|
| 96 |
{
|
| 97 |
"file": "0006.png",
|
| 98 |
-
"
|
|
|
|
| 99 |
"wer": 1.5,
|
| 100 |
-
"digit_cer": 0.
|
| 101 |
"cjk_cer": 0.0,
|
| 102 |
"repetition_rate": 0.0,
|
| 103 |
"table_score": 1.0,
|
| 104 |
-
"elapsed_seconds":
|
| 105 |
-
"ref_length":
|
| 106 |
-
"hyp_length":
|
| 107 |
-
"ref_digit_count":
|
| 108 |
"ref_cjk_count": 0
|
| 109 |
},
|
| 110 |
{
|
| 111 |
"file": "0007.png",
|
| 112 |
-
"
|
|
|
|
| 113 |
"wer": 1.5,
|
| 114 |
-
"digit_cer": 0.
|
| 115 |
"cjk_cer": 0.0,
|
| 116 |
"repetition_rate": 0.0,
|
| 117 |
"table_score": 1.0,
|
| 118 |
-
"elapsed_seconds":
|
| 119 |
-
"ref_length":
|
| 120 |
-
"hyp_length":
|
| 121 |
-
"ref_digit_count":
|
| 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":
|
| 133 |
-
"ref_length":
|
| 134 |
-
"hyp_length":
|
| 135 |
-
"ref_digit_count":
|
| 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":
|
| 147 |
-
"ref_length":
|
| 148 |
-
"hyp_length":
|
| 149 |
-
"ref_digit_count":
|
| 150 |
"ref_cjk_count": 42
|
| 151 |
},
|
| 152 |
{
|
| 153 |
"file": "0010.png",
|
| 154 |
-
"
|
| 155 |
-
"
|
|
|
|
| 156 |
"digit_cer": 0.0,
|
| 157 |
"cjk_cer": 1.0,
|
| 158 |
"repetition_rate": 0.0,
|
| 159 |
"table_score": null,
|
| 160 |
-
"elapsed_seconds":
|
| 161 |
-
"ref_length":
|
| 162 |
-
"hyp_length":
|
| 163 |
-
"ref_digit_count":
|
| 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":
|
| 175 |
-
"ref_length":
|
| 176 |
-
"hyp_length":
|
| 177 |
-
"ref_digit_count":
|
| 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":
|
| 189 |
-
"ref_length":
|
| 190 |
-
"hyp_length":
|
| 191 |
-
"ref_digit_count":
|
| 192 |
"ref_cjk_count": 0
|
| 193 |
}
|
| 194 |
]
|
|
|
|
| 1 |
{
|
| 2 |
"model_path": "Unlimited-OCR",
|
| 3 |
+
"served_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 4 |
+
"backend": "mlx",
|
| 5 |
"prompt": "<image>document parsing.",
|
| 6 |
"max_tokens": 1024,
|
| 7 |
"profile": "accurate",
|
|
|
|
| 14 |
},
|
| 15 |
"num_images": 12,
|
| 16 |
"num_samples": 12,
|
| 17 |
+
"total_time_seconds": 169.77583128899278,
|
| 18 |
+
"mean_cer": 0.205212265458972,
|
| 19 |
+
"mean_wer": 0.5919915970344224,
|
| 20 |
"num_digit_samples": 12,
|
| 21 |
"num_cjk_samples": 3,
|
| 22 |
+
"mean_digit_cer": 0.11330969496189873,
|
| 23 |
"mean_cjk_cer": 0.3333333333333333,
|
| 24 |
"num_table_samples": 3,
|
| 25 |
"mean_table_score": 1.0,
|
| 26 |
"mean_repetition_rate": 0.0,
|
| 27 |
+
"by_category": {
|
| 28 |
+
"cjk_document": {
|
| 29 |
+
"num_samples": 3,
|
| 30 |
+
"mean_cer": 0.18681318681318682,
|
| 31 |
+
"mean_digit_cer": 0.0,
|
| 32 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 33 |
+
"mean_table_score": null,
|
| 34 |
+
"num_digit_samples": 3,
|
| 35 |
+
"num_cjk_samples": 3,
|
| 36 |
+
"num_table_samples": 0
|
| 37 |
+
},
|
| 38 |
+
"english_document": {
|
| 39 |
+
"num_samples": 4,
|
| 40 |
+
"mean_cer": 0.34397793132397353,
|
| 41 |
+
"mean_digit_cer": 0.1993040848856962,
|
| 42 |
+
"mean_cjk_cer": null,
|
| 43 |
+
"mean_table_score": null,
|
| 44 |
+
"num_digit_samples": 4,
|
| 45 |
+
"num_cjk_samples": 0,
|
| 46 |
+
"num_table_samples": 0
|
| 47 |
+
},
|
| 48 |
+
"low_quality_scan": {
|
| 49 |
+
"num_samples": 2,
|
| 50 |
+
"mean_cer": 0.0,
|
| 51 |
+
"mean_digit_cer": 0.0,
|
| 52 |
+
"mean_cjk_cer": null,
|
| 53 |
+
"mean_table_score": null,
|
| 54 |
+
"num_digit_samples": 2,
|
| 55 |
+
"num_cjk_samples": 0,
|
| 56 |
+
"num_table_samples": 0
|
| 57 |
+
},
|
| 58 |
+
"table_numeric": {
|
| 59 |
+
"num_samples": 3,
|
| 60 |
+
"mean_cer": 0.1753986332574032,
|
| 61 |
+
"mean_digit_cer": 0.1875,
|
| 62 |
+
"mean_cjk_cer": null,
|
| 63 |
+
"mean_table_score": 1.0,
|
| 64 |
+
"num_digit_samples": 3,
|
| 65 |
+
"num_cjk_samples": 0,
|
| 66 |
+
"num_table_samples": 3
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
"per_file": [
|
| 70 |
{
|
| 71 |
"file": "0001.png",
|
| 72 |
+
"category": "english_document",
|
| 73 |
+
"cer": 0.3923145665773012,
|
| 74 |
+
"wer": 0.3317757009345794,
|
| 75 |
+
"digit_cer": 0.18518518518518517,
|
| 76 |
"cjk_cer": 0.0,
|
| 77 |
"repetition_rate": 0.0,
|
| 78 |
"table_score": null,
|
| 79 |
+
"elapsed_seconds": 25.946757499987143,
|
| 80 |
+
"ref_length": 1332,
|
| 81 |
+
"hyp_length": 1719,
|
| 82 |
+
"ref_digit_count": 27,
|
| 83 |
"ref_cjk_count": 0
|
| 84 |
},
|
| 85 |
{
|
| 86 |
"file": "0002.png",
|
| 87 |
+
"category": "english_document",
|
| 88 |
+
"cer": 0.37906137184115524,
|
| 89 |
+
"wer": 0.36486486486486486,
|
| 90 |
+
"digit_cer": 0.1016949152542373,
|
| 91 |
"cjk_cer": 0.0,
|
| 92 |
"repetition_rate": 0.0,
|
| 93 |
"table_score": null,
|
| 94 |
+
"elapsed_seconds": 27.539474832999986,
|
| 95 |
+
"ref_length": 1329,
|
| 96 |
+
"hyp_length": 1710,
|
| 97 |
+
"ref_digit_count": 59,
|
| 98 |
"ref_cjk_count": 0
|
| 99 |
},
|
| 100 |
{
|
| 101 |
"file": "0003.png",
|
| 102 |
+
"category": "english_document",
|
| 103 |
+
"cer": 0.525911708253359,
|
| 104 |
+
"wer": 0.46551724137931033,
|
| 105 |
+
"digit_cer": 0.273972602739726,
|
| 106 |
"cjk_cer": 0.0,
|
| 107 |
"repetition_rate": 0.0,
|
| 108 |
"table_score": null,
|
| 109 |
+
"elapsed_seconds": 28.937275707998197,
|
| 110 |
+
"ref_length": 636,
|
| 111 |
+
"hyp_length": 824,
|
| 112 |
+
"ref_digit_count": 73,
|
| 113 |
"ref_cjk_count": 0
|
| 114 |
},
|
| 115 |
{
|
| 116 |
"file": "0004.png",
|
| 117 |
+
"category": "english_document",
|
| 118 |
+
"cer": 0.07862407862407862,
|
| 119 |
+
"wer": 0.16901408450704225,
|
| 120 |
+
"digit_cer": 0.23636363636363636,
|
| 121 |
"cjk_cer": 0.0,
|
| 122 |
"repetition_rate": 0.0,
|
| 123 |
"table_score": null,
|
| 124 |
+
"elapsed_seconds": 14.294532250001794,
|
| 125 |
+
"ref_length": 477,
|
| 126 |
+
"hyp_length": 476,
|
| 127 |
+
"ref_digit_count": 55,
|
| 128 |
"ref_cjk_count": 0
|
| 129 |
},
|
| 130 |
{
|
| 131 |
"file": "0005.png",
|
| 132 |
+
"category": "table_numeric",
|
| 133 |
+
"cer": 0.17539863325740318,
|
| 134 |
"wer": 1.5,
|
| 135 |
+
"digit_cer": 0.1875,
|
| 136 |
"cjk_cer": 0.0,
|
| 137 |
"repetition_rate": 0.0,
|
| 138 |
"table_score": 1.0,
|
| 139 |
+
"elapsed_seconds": 12.845850040990626,
|
| 140 |
+
"ref_length": 446,
|
| 141 |
+
"hyp_length": 519,
|
| 142 |
+
"ref_digit_count": 80,
|
| 143 |
"ref_cjk_count": 0
|
| 144 |
},
|
| 145 |
{
|
| 146 |
"file": "0006.png",
|
| 147 |
+
"category": "table_numeric",
|
| 148 |
+
"cer": 0.17539863325740318,
|
| 149 |
"wer": 1.5,
|
| 150 |
+
"digit_cer": 0.1875,
|
| 151 |
"cjk_cer": 0.0,
|
| 152 |
"repetition_rate": 0.0,
|
| 153 |
"table_score": 1.0,
|
| 154 |
+
"elapsed_seconds": 12.608022582993726,
|
| 155 |
+
"ref_length": 446,
|
| 156 |
+
"hyp_length": 519,
|
| 157 |
+
"ref_digit_count": 80,
|
| 158 |
"ref_cjk_count": 0
|
| 159 |
},
|
| 160 |
{
|
| 161 |
"file": "0007.png",
|
| 162 |
+
"category": "table_numeric",
|
| 163 |
+
"cer": 0.17539863325740318,
|
| 164 |
"wer": 1.5,
|
| 165 |
+
"digit_cer": 0.1875,
|
| 166 |
"cjk_cer": 0.0,
|
| 167 |
"repetition_rate": 0.0,
|
| 168 |
"table_score": 1.0,
|
| 169 |
+
"elapsed_seconds": 12.634055832997547,
|
| 170 |
+
"ref_length": 446,
|
| 171 |
+
"hyp_length": 519,
|
| 172 |
+
"ref_digit_count": 80,
|
| 173 |
"ref_cjk_count": 0
|
| 174 |
},
|
| 175 |
{
|
| 176 |
"file": "0008.png",
|
| 177 |
+
"category": "cjk_document",
|
| 178 |
"cer": 0.0,
|
| 179 |
"wer": 0.0,
|
| 180 |
"digit_cer": 0.0,
|
| 181 |
"cjk_cer": 0.0,
|
| 182 |
"repetition_rate": 0.0,
|
| 183 |
"table_score": null,
|
| 184 |
+
"elapsed_seconds": 7.23521095799515,
|
| 185 |
+
"ref_length": 98,
|
| 186 |
+
"hyp_length": 98,
|
| 187 |
+
"ref_digit_count": 30,
|
| 188 |
"ref_cjk_count": 41
|
| 189 |
},
|
| 190 |
{
|
| 191 |
"file": "0009.png",
|
| 192 |
+
"category": "cjk_document",
|
| 193 |
"cer": 0.0,
|
| 194 |
"wer": 0.0,
|
| 195 |
"digit_cer": 0.0,
|
| 196 |
"cjk_cer": 0.0,
|
| 197 |
"repetition_rate": 0.0,
|
| 198 |
"table_score": null,
|
| 199 |
+
"elapsed_seconds": 7.373159375012619,
|
| 200 |
+
"ref_length": 100,
|
| 201 |
+
"hyp_length": 100,
|
| 202 |
+
"ref_digit_count": 30,
|
| 203 |
"ref_cjk_count": 42
|
| 204 |
},
|
| 205 |
{
|
| 206 |
"file": "0010.png",
|
| 207 |
+
"category": "cjk_document",
|
| 208 |
+
"cer": 0.5604395604395604,
|
| 209 |
+
"wer": 1.2727272727272727,
|
| 210 |
"digit_cer": 0.0,
|
| 211 |
"cjk_cer": 1.0,
|
| 212 |
"repetition_rate": 0.0,
|
| 213 |
"table_score": null,
|
| 214 |
+
"elapsed_seconds": 7.378904458004399,
|
| 215 |
+
"ref_length": 112,
|
| 216 |
+
"hyp_length": 126,
|
| 217 |
+
"ref_digit_count": 30,
|
| 218 |
"ref_cjk_count": 44
|
| 219 |
},
|
| 220 |
{
|
| 221 |
"file": "0011.png",
|
| 222 |
+
"category": "low_quality_scan",
|
| 223 |
"cer": 0.0,
|
| 224 |
"wer": 0.0,
|
| 225 |
"digit_cer": 0.0,
|
| 226 |
"cjk_cer": 0.0,
|
| 227 |
"repetition_rate": 0.0,
|
| 228 |
"table_score": null,
|
| 229 |
+
"elapsed_seconds": 6.398878874999355,
|
| 230 |
+
"ref_length": 120,
|
| 231 |
+
"hyp_length": 120,
|
| 232 |
+
"ref_digit_count": 29,
|
| 233 |
"ref_cjk_count": 0
|
| 234 |
},
|
| 235 |
{
|
| 236 |
"file": "0012.png",
|
| 237 |
+
"category": "low_quality_scan",
|
| 238 |
"cer": 0.0,
|
| 239 |
"wer": 0.0,
|
| 240 |
"digit_cer": 0.0,
|
| 241 |
"cjk_cer": 0.0,
|
| 242 |
"repetition_rate": 0.0,
|
| 243 |
"table_score": null,
|
| 244 |
+
"elapsed_seconds": 6.583708875012235,
|
| 245 |
+
"ref_length": 120,
|
| 246 |
+
"hyp_length": 120,
|
| 247 |
+
"ref_digit_count": 29,
|
| 248 |
"ref_cjk_count": 0
|
| 249 |
}
|
| 250 |
]
|
release/calibration_affine8_accuracy.json
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8",
|
| 3 |
+
"served_revision": null,
|
| 4 |
+
"backend": "mlx",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 1024,
|
| 7 |
+
"profile": "accurate",
|
| 8 |
+
"generation_settings": {
|
| 9 |
+
"temperature": 0.0,
|
| 10 |
+
"top_p": 1.0,
|
| 11 |
+
"repetition_penalty": 1.0,
|
| 12 |
+
"no_repeat_ngram_size": 35,
|
| 13 |
+
"ngram_window": 128
|
| 14 |
+
},
|
| 15 |
+
"num_images": 10,
|
| 16 |
+
"num_samples": 10,
|
| 17 |
+
"total_time_seconds": 121.92750062300183,
|
| 18 |
+
"mean_cer": 0.19105830313811087,
|
| 19 |
+
"mean_wer": 0.647581477892157,
|
| 20 |
+
"num_digit_samples": 10,
|
| 21 |
+
"num_cjk_samples": 3,
|
| 22 |
+
"mean_digit_cer": 0.07857558139534884,
|
| 23 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 24 |
+
"num_table_samples": 3,
|
| 25 |
+
"mean_table_score": 1.0,
|
| 26 |
+
"mean_repetition_rate": 0.0,
|
| 27 |
+
"by_category": {
|
| 28 |
+
"cjk_document": {
|
| 29 |
+
"num_samples": 3,
|
| 30 |
+
"mean_cer": 0.18315018315018317,
|
| 31 |
+
"mean_digit_cer": 0.0,
|
| 32 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 33 |
+
"mean_table_score": null,
|
| 34 |
+
"num_digit_samples": 3,
|
| 35 |
+
"num_cjk_samples": 3,
|
| 36 |
+
"num_table_samples": 0
|
| 37 |
+
},
|
| 38 |
+
"english_document": {
|
| 39 |
+
"num_samples": 2,
|
| 40 |
+
"mean_cer": 0.4174682910791748,
|
| 41 |
+
"mean_digit_cer": 0.11162790697674418,
|
| 42 |
+
"mean_cjk_cer": null,
|
| 43 |
+
"mean_table_score": null,
|
| 44 |
+
"num_digit_samples": 2,
|
| 45 |
+
"num_cjk_samples": 0,
|
| 46 |
+
"num_table_samples": 0
|
| 47 |
+
},
|
| 48 |
+
"low_quality_scan": {
|
| 49 |
+
"num_samples": 2,
|
| 50 |
+
"mean_cer": 0.0,
|
| 51 |
+
"mean_digit_cer": 0.0,
|
| 52 |
+
"mean_cjk_cer": null,
|
| 53 |
+
"mean_table_score": null,
|
| 54 |
+
"num_digit_samples": 2,
|
| 55 |
+
"num_cjk_samples": 0,
|
| 56 |
+
"num_table_samples": 0
|
| 57 |
+
},
|
| 58 |
+
"table_numeric": {
|
| 59 |
+
"num_samples": 3,
|
| 60 |
+
"mean_cer": 0.1753986332574032,
|
| 61 |
+
"mean_digit_cer": 0.1875,
|
| 62 |
+
"mean_cjk_cer": null,
|
| 63 |
+
"mean_table_score": 1.0,
|
| 64 |
+
"num_digit_samples": 3,
|
| 65 |
+
"num_cjk_samples": 0,
|
| 66 |
+
"num_table_samples": 3
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"per_file": [
|
| 70 |
+
{
|
| 71 |
+
"file": "0001.png",
|
| 72 |
+
"category": "english_document",
|
| 73 |
+
"cer": 0.08904719501335707,
|
| 74 |
+
"wer": 0.24663677130044842,
|
| 75 |
+
"digit_cer": 0.023255813953488372,
|
| 76 |
+
"cjk_cer": 0.0,
|
| 77 |
+
"repetition_rate": 0.0,
|
| 78 |
+
"table_score": null,
|
| 79 |
+
"elapsed_seconds": 22.065639082997222,
|
| 80 |
+
"ref_length": 1345,
|
| 81 |
+
"hyp_length": 1259,
|
| 82 |
+
"ref_digit_count": 86,
|
| 83 |
+
"ref_cjk_count": 0
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"file": "0002.png",
|
| 87 |
+
"category": "english_document",
|
| 88 |
+
"cer": 0.7458893871449925,
|
| 89 |
+
"wer": 0.592814371257485,
|
| 90 |
+
"digit_cer": 0.2,
|
| 91 |
+
"cjk_cer": 0.0,
|
| 92 |
+
"repetition_rate": 0.0,
|
| 93 |
+
"table_score": null,
|
| 94 |
+
"elapsed_seconds": 31.2265478749905,
|
| 95 |
+
"ref_length": 835,
|
| 96 |
+
"hyp_length": 1275,
|
| 97 |
+
"ref_digit_count": 95,
|
| 98 |
+
"ref_cjk_count": 0
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"file": "0003.png",
|
| 102 |
+
"category": "table_numeric",
|
| 103 |
+
"cer": 0.17539863325740318,
|
| 104 |
+
"wer": 1.5,
|
| 105 |
+
"digit_cer": 0.1875,
|
| 106 |
+
"cjk_cer": 0.0,
|
| 107 |
+
"repetition_rate": 0.0,
|
| 108 |
+
"table_score": 1.0,
|
| 109 |
+
"elapsed_seconds": 11.632652583008166,
|
| 110 |
+
"ref_length": 446,
|
| 111 |
+
"hyp_length": 519,
|
| 112 |
+
"ref_digit_count": 80,
|
| 113 |
+
"ref_cjk_count": 0
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"file": "0004.png",
|
| 117 |
+
"category": "table_numeric",
|
| 118 |
+
"cer": 0.17539863325740318,
|
| 119 |
+
"wer": 1.5,
|
| 120 |
+
"digit_cer": 0.1875,
|
| 121 |
+
"cjk_cer": 0.0,
|
| 122 |
+
"repetition_rate": 0.0,
|
| 123 |
+
"table_score": 1.0,
|
| 124 |
+
"elapsed_seconds": 11.664347833007923,
|
| 125 |
+
"ref_length": 446,
|
| 126 |
+
"hyp_length": 519,
|
| 127 |
+
"ref_digit_count": 80,
|
| 128 |
+
"ref_cjk_count": 0
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"file": "0005.png",
|
| 132 |
+
"category": "table_numeric",
|
| 133 |
+
"cer": 0.17539863325740318,
|
| 134 |
+
"wer": 1.5,
|
| 135 |
+
"digit_cer": 0.1875,
|
| 136 |
+
"cjk_cer": 0.0,
|
| 137 |
+
"repetition_rate": 0.0,
|
| 138 |
+
"table_score": 1.0,
|
| 139 |
+
"elapsed_seconds": 11.887310291000176,
|
| 140 |
+
"ref_length": 446,
|
| 141 |
+
"hyp_length": 519,
|
| 142 |
+
"ref_digit_count": 80,
|
| 143 |
+
"ref_cjk_count": 0
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"file": "0006.png",
|
| 147 |
+
"category": "cjk_document",
|
| 148 |
+
"cer": 0.0,
|
| 149 |
+
"wer": 0.0,
|
| 150 |
+
"digit_cer": 0.0,
|
| 151 |
+
"cjk_cer": 0.0,
|
| 152 |
+
"repetition_rate": 0.0,
|
| 153 |
+
"table_score": null,
|
| 154 |
+
"elapsed_seconds": 6.9882367919926764,
|
| 155 |
+
"ref_length": 98,
|
| 156 |
+
"hyp_length": 98,
|
| 157 |
+
"ref_digit_count": 30,
|
| 158 |
+
"ref_cjk_count": 41
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"file": "0007.png",
|
| 162 |
+
"category": "cjk_document",
|
| 163 |
+
"cer": 0.0,
|
| 164 |
+
"wer": 0.0,
|
| 165 |
+
"digit_cer": 0.0,
|
| 166 |
+
"cjk_cer": 0.0,
|
| 167 |
+
"repetition_rate": 0.0,
|
| 168 |
+
"table_score": null,
|
| 169 |
+
"elapsed_seconds": 6.911115791008342,
|
| 170 |
+
"ref_length": 100,
|
| 171 |
+
"hyp_length": 100,
|
| 172 |
+
"ref_digit_count": 30,
|
| 173 |
+
"ref_cjk_count": 42
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"file": "0008.png",
|
| 177 |
+
"category": "cjk_document",
|
| 178 |
+
"cer": 0.5494505494505495,
|
| 179 |
+
"wer": 1.1363636363636365,
|
| 180 |
+
"digit_cer": 0.0,
|
| 181 |
+
"cjk_cer": 1.0,
|
| 182 |
+
"repetition_rate": 0.0,
|
| 183 |
+
"table_score": null,
|
| 184 |
+
"elapsed_seconds": 7.001193958989461,
|
| 185 |
+
"ref_length": 112,
|
| 186 |
+
"hyp_length": 120,
|
| 187 |
+
"ref_digit_count": 30,
|
| 188 |
+
"ref_cjk_count": 44
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"file": "0009.png",
|
| 192 |
+
"category": "low_quality_scan",
|
| 193 |
+
"cer": 0.0,
|
| 194 |
+
"wer": 0.0,
|
| 195 |
+
"digit_cer": 0.0,
|
| 196 |
+
"cjk_cer": 0.0,
|
| 197 |
+
"repetition_rate": 0.0,
|
| 198 |
+
"table_score": null,
|
| 199 |
+
"elapsed_seconds": 6.5204695830034325,
|
| 200 |
+
"ref_length": 120,
|
| 201 |
+
"hyp_length": 120,
|
| 202 |
+
"ref_digit_count": 29,
|
| 203 |
+
"ref_cjk_count": 0
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"file": "0010.png",
|
| 207 |
+
"category": "low_quality_scan",
|
| 208 |
+
"cer": 0.0,
|
| 209 |
+
"wer": 0.0,
|
| 210 |
+
"digit_cer": 0.0,
|
| 211 |
+
"cjk_cer": 0.0,
|
| 212 |
+
"repetition_rate": 0.0,
|
| 213 |
+
"table_score": null,
|
| 214 |
+
"elapsed_seconds": 6.029986833003932,
|
| 215 |
+
"ref_length": 120,
|
| 216 |
+
"hyp_length": 120,
|
| 217 |
+
"ref_digit_count": 29,
|
| 218 |
+
"ref_cjk_count": 0
|
| 219 |
+
}
|
| 220 |
+
]
|
| 221 |
+
}
|
release/calibration_affine8_performance.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8",
|
| 3 |
+
"served_revision": null,
|
| 4 |
+
"image_path": "test_invoice.png",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 256,
|
| 7 |
+
"system": {
|
| 8 |
+
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 9 |
+
"processor": "arm",
|
| 10 |
+
"python_version": "3.14.6",
|
| 11 |
+
"machine": "arm64",
|
| 12 |
+
"mlx_version": "0.32.0",
|
| 13 |
+
"mlx_vlm_version": "0.6.6",
|
| 14 |
+
"chip": "Apple M3 Max",
|
| 15 |
+
"total_memory_gb": 128.0
|
| 16 |
+
},
|
| 17 |
+
"model_load_time_seconds": 1.0313333749945741,
|
| 18 |
+
"memory_after_load_mb": 4098.546875,
|
| 19 |
+
"num_warmup": 1,
|
| 20 |
+
"num_runs": 3,
|
| 21 |
+
"mean_tps": 39.58614991640946,
|
| 22 |
+
"std_tps": 0.7627654762339389,
|
| 23 |
+
"mean_elapsed_seconds": 8.051595569670704,
|
| 24 |
+
"mean_peak_memory_mb": 5275.928047,
|
| 25 |
+
"runs": [
|
| 26 |
+
{
|
| 27 |
+
"run": 1,
|
| 28 |
+
"elapsed_seconds": 7.985530750011094,
|
| 29 |
+
"tokens_generated": 256,
|
| 30 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 31 |
+
"tokens_per_second": 40.03117010793416,
|
| 32 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 33 |
+
"peak_memory_mb": 5275.752407,
|
| 34 |
+
"memory_delta_mb": 1337.1867550000002,
|
| 35 |
+
"process_peak_rss_mb": 4120.59375,
|
| 36 |
+
"prompt_tokens": 697,
|
| 37 |
+
"prompt_tokens_per_second": 453.0235706986471,
|
| 38 |
+
"finish_reason": "length"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"run": 2,
|
| 42 |
+
"elapsed_seconds": 7.846619042000384,
|
| 43 |
+
"tokens_generated": 256,
|
| 44 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 45 |
+
"tokens_per_second": 40.21463014341998,
|
| 46 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 47 |
+
"peak_memory_mb": 5276.015867,
|
| 48 |
+
"memory_delta_mb": 1337.188071,
|
| 49 |
+
"process_peak_rss_mb": 4121.078125,
|
| 50 |
+
"prompt_tokens": 697,
|
| 51 |
+
"prompt_tokens_per_second": 487.66087944129447,
|
| 52 |
+
"finish_reason": "length"
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"run": 3,
|
| 56 |
+
"elapsed_seconds": 8.322636917000636,
|
| 57 |
+
"tokens_generated": 256,
|
| 58 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 59 |
+
"tokens_per_second": 38.51264949787424,
|
| 60 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 61 |
+
"peak_memory_mb": 5276.015867,
|
| 62 |
+
"memory_delta_mb": 1337.188071,
|
| 63 |
+
"process_peak_rss_mb": 4121.21875,
|
| 64 |
+
"prompt_tokens": 697,
|
| 65 |
+
"prompt_tokens_per_second": 423.58096065031117,
|
| 66 |
+
"finish_reason": "length"
|
| 67 |
+
}
|
| 68 |
+
]
|
| 69 |
+
}
|
release/calibration_baseline_accuracy.json
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "Unlimited-OCR",
|
| 3 |
+
"served_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 4 |
+
"backend": "mlx",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 1024,
|
| 7 |
+
"profile": "accurate",
|
| 8 |
+
"generation_settings": {
|
| 9 |
+
"temperature": 0.0,
|
| 10 |
+
"top_p": 1.0,
|
| 11 |
+
"repetition_penalty": 1.0,
|
| 12 |
+
"no_repeat_ngram_size": 35,
|
| 13 |
+
"ngram_window": 128
|
| 14 |
+
},
|
| 15 |
+
"num_images": 10,
|
| 16 |
+
"num_samples": 10,
|
| 17 |
+
"total_time_seconds": 121.70595645793946,
|
| 18 |
+
"mean_cer": 0.20230911688823577,
|
| 19 |
+
"mean_wer": 0.6426630229146273,
|
| 20 |
+
"num_digit_samples": 10,
|
| 21 |
+
"num_cjk_samples": 3,
|
| 22 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 23 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 24 |
+
"num_table_samples": 3,
|
| 25 |
+
"mean_table_score": 1.0,
|
| 26 |
+
"mean_repetition_rate": 0.0,
|
| 27 |
+
"by_category": {
|
| 28 |
+
"cjk_document": {
|
| 29 |
+
"num_samples": 3,
|
| 30 |
+
"mean_cer": 0.18681318681318682,
|
| 31 |
+
"mean_digit_cer": 0.0,
|
| 32 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 33 |
+
"mean_table_score": null,
|
| 34 |
+
"num_digit_samples": 3,
|
| 35 |
+
"num_cjk_samples": 3,
|
| 36 |
+
"num_table_samples": 0
|
| 37 |
+
},
|
| 38 |
+
"english_document": {
|
| 39 |
+
"num_samples": 2,
|
| 40 |
+
"mean_cer": 0.46822785433529396,
|
| 41 |
+
"mean_digit_cer": 0.08059975520195839,
|
| 42 |
+
"mean_cjk_cer": null,
|
| 43 |
+
"mean_table_score": null,
|
| 44 |
+
"num_digit_samples": 2,
|
| 45 |
+
"num_cjk_samples": 0,
|
| 46 |
+
"num_table_samples": 0
|
| 47 |
+
},
|
| 48 |
+
"low_quality_scan": {
|
| 49 |
+
"num_samples": 2,
|
| 50 |
+
"mean_cer": 0.0,
|
| 51 |
+
"mean_digit_cer": 0.0,
|
| 52 |
+
"mean_cjk_cer": null,
|
| 53 |
+
"mean_table_score": null,
|
| 54 |
+
"num_digit_samples": 2,
|
| 55 |
+
"num_cjk_samples": 0,
|
| 56 |
+
"num_table_samples": 0
|
| 57 |
+
},
|
| 58 |
+
"table_numeric": {
|
| 59 |
+
"num_samples": 3,
|
| 60 |
+
"mean_cer": 0.1753986332574032,
|
| 61 |
+
"mean_digit_cer": 0.1875,
|
| 62 |
+
"mean_cjk_cer": null,
|
| 63 |
+
"mean_table_score": 1.0,
|
| 64 |
+
"num_digit_samples": 3,
|
| 65 |
+
"num_cjk_samples": 0,
|
| 66 |
+
"num_table_samples": 3
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"per_file": [
|
| 70 |
+
{
|
| 71 |
+
"file": "0001.png",
|
| 72 |
+
"category": "english_document",
|
| 73 |
+
"cer": 0.02315227070347284,
|
| 74 |
+
"wer": 0.19282511210762332,
|
| 75 |
+
"digit_cer": 0.03488372093023256,
|
| 76 |
+
"cjk_cer": 0.0,
|
| 77 |
+
"repetition_rate": 0.0,
|
| 78 |
+
"table_score": null,
|
| 79 |
+
"elapsed_seconds": 23.124094749990036,
|
| 80 |
+
"ref_length": 1345,
|
| 81 |
+
"hyp_length": 1348,
|
| 82 |
+
"ref_digit_count": 86,
|
| 83 |
+
"ref_cjk_count": 0
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"file": "0002.png",
|
| 87 |
+
"category": "english_document",
|
| 88 |
+
"cer": 0.9133034379671151,
|
| 89 |
+
"wer": 0.46107784431137727,
|
| 90 |
+
"digit_cer": 0.12631578947368421,
|
| 91 |
+
"cjk_cer": 0.0,
|
| 92 |
+
"repetition_rate": 0.0,
|
| 93 |
+
"table_score": null,
|
| 94 |
+
"elapsed_seconds": 31.42361595899274,
|
| 95 |
+
"ref_length": 835,
|
| 96 |
+
"hyp_length": 1378,
|
| 97 |
+
"ref_digit_count": 95,
|
| 98 |
+
"ref_cjk_count": 0
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"file": "0003.png",
|
| 102 |
+
"category": "table_numeric",
|
| 103 |
+
"cer": 0.17539863325740318,
|
| 104 |
+
"wer": 1.5,
|
| 105 |
+
"digit_cer": 0.1875,
|
| 106 |
+
"cjk_cer": 0.0,
|
| 107 |
+
"repetition_rate": 0.0,
|
| 108 |
+
"table_score": 1.0,
|
| 109 |
+
"elapsed_seconds": 11.507403457988403,
|
| 110 |
+
"ref_length": 446,
|
| 111 |
+
"hyp_length": 519,
|
| 112 |
+
"ref_digit_count": 80,
|
| 113 |
+
"ref_cjk_count": 0
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"file": "0004.png",
|
| 117 |
+
"category": "table_numeric",
|
| 118 |
+
"cer": 0.17539863325740318,
|
| 119 |
+
"wer": 1.5,
|
| 120 |
+
"digit_cer": 0.1875,
|
| 121 |
+
"cjk_cer": 0.0,
|
| 122 |
+
"repetition_rate": 0.0,
|
| 123 |
+
"table_score": 1.0,
|
| 124 |
+
"elapsed_seconds": 11.631738583993865,
|
| 125 |
+
"ref_length": 446,
|
| 126 |
+
"hyp_length": 519,
|
| 127 |
+
"ref_digit_count": 80,
|
| 128 |
+
"ref_cjk_count": 0
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"file": "0005.png",
|
| 132 |
+
"category": "table_numeric",
|
| 133 |
+
"cer": 0.17539863325740318,
|
| 134 |
+
"wer": 1.5,
|
| 135 |
+
"digit_cer": 0.1875,
|
| 136 |
+
"cjk_cer": 0.0,
|
| 137 |
+
"repetition_rate": 0.0,
|
| 138 |
+
"table_score": 1.0,
|
| 139 |
+
"elapsed_seconds": 11.604318874989985,
|
| 140 |
+
"ref_length": 446,
|
| 141 |
+
"hyp_length": 519,
|
| 142 |
+
"ref_digit_count": 80,
|
| 143 |
+
"ref_cjk_count": 0
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"file": "0006.png",
|
| 147 |
+
"category": "cjk_document",
|
| 148 |
+
"cer": 0.0,
|
| 149 |
+
"wer": 0.0,
|
| 150 |
+
"digit_cer": 0.0,
|
| 151 |
+
"cjk_cer": 0.0,
|
| 152 |
+
"repetition_rate": 0.0,
|
| 153 |
+
"table_score": null,
|
| 154 |
+
"elapsed_seconds": 6.63977562499349,
|
| 155 |
+
"ref_length": 98,
|
| 156 |
+
"hyp_length": 98,
|
| 157 |
+
"ref_digit_count": 30,
|
| 158 |
+
"ref_cjk_count": 41
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"file": "0007.png",
|
| 162 |
+
"category": "cjk_document",
|
| 163 |
+
"cer": 0.0,
|
| 164 |
+
"wer": 0.0,
|
| 165 |
+
"digit_cer": 0.0,
|
| 166 |
+
"cjk_cer": 0.0,
|
| 167 |
+
"repetition_rate": 0.0,
|
| 168 |
+
"table_score": null,
|
| 169 |
+
"elapsed_seconds": 6.620961624998017,
|
| 170 |
+
"ref_length": 100,
|
| 171 |
+
"hyp_length": 100,
|
| 172 |
+
"ref_digit_count": 30,
|
| 173 |
+
"ref_cjk_count": 42
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"file": "0008.png",
|
| 177 |
+
"category": "cjk_document",
|
| 178 |
+
"cer": 0.5604395604395604,
|
| 179 |
+
"wer": 1.2727272727272727,
|
| 180 |
+
"digit_cer": 0.0,
|
| 181 |
+
"cjk_cer": 1.0,
|
| 182 |
+
"repetition_rate": 0.0,
|
| 183 |
+
"table_score": null,
|
| 184 |
+
"elapsed_seconds": 6.897685666001053,
|
| 185 |
+
"ref_length": 112,
|
| 186 |
+
"hyp_length": 126,
|
| 187 |
+
"ref_digit_count": 30,
|
| 188 |
+
"ref_cjk_count": 44
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"file": "0009.png",
|
| 192 |
+
"category": "low_quality_scan",
|
| 193 |
+
"cer": 0.0,
|
| 194 |
+
"wer": 0.0,
|
| 195 |
+
"digit_cer": 0.0,
|
| 196 |
+
"cjk_cer": 0.0,
|
| 197 |
+
"repetition_rate": 0.0,
|
| 198 |
+
"table_score": null,
|
| 199 |
+
"elapsed_seconds": 6.136604707993683,
|
| 200 |
+
"ref_length": 120,
|
| 201 |
+
"hyp_length": 120,
|
| 202 |
+
"ref_digit_count": 29,
|
| 203 |
+
"ref_cjk_count": 0
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"file": "0010.png",
|
| 207 |
+
"category": "low_quality_scan",
|
| 208 |
+
"cer": 0.0,
|
| 209 |
+
"wer": 0.0,
|
| 210 |
+
"digit_cer": 0.0,
|
| 211 |
+
"cjk_cer": 0.0,
|
| 212 |
+
"repetition_rate": 0.0,
|
| 213 |
+
"table_score": null,
|
| 214 |
+
"elapsed_seconds": 6.119757207998191,
|
| 215 |
+
"ref_length": 120,
|
| 216 |
+
"hyp_length": 120,
|
| 217 |
+
"ref_digit_count": 29,
|
| 218 |
+
"ref_cjk_count": 0
|
| 219 |
+
}
|
| 220 |
+
]
|
| 221 |
+
}
|
release/calibration_bfloat16_accuracy.json
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16",
|
| 3 |
+
"served_revision": null,
|
| 4 |
+
"backend": "mlx",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 1024,
|
| 7 |
+
"profile": "accurate",
|
| 8 |
+
"generation_settings": {
|
| 9 |
+
"temperature": 0.0,
|
| 10 |
+
"top_p": 1.0,
|
| 11 |
+
"repetition_penalty": 1.0,
|
| 12 |
+
"no_repeat_ngram_size": 35,
|
| 13 |
+
"ngram_window": 128
|
| 14 |
+
},
|
| 15 |
+
"num_images": 10,
|
| 16 |
+
"num_samples": 10,
|
| 17 |
+
"total_time_seconds": 130.78621862299042,
|
| 18 |
+
"mean_cer": 0.19010066585760363,
|
| 19 |
+
"mean_wer": 0.6393071175219882,
|
| 20 |
+
"num_digit_samples": 10,
|
| 21 |
+
"num_cjk_samples": 3,
|
| 22 |
+
"mean_digit_cer": 0.06278610771113831,
|
| 23 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 24 |
+
"num_table_samples": 3,
|
| 25 |
+
"mean_table_score": 1.0,
|
| 26 |
+
"mean_repetition_rate": 0.0,
|
| 27 |
+
"by_category": {
|
| 28 |
+
"cjk_document": {
|
| 29 |
+
"num_samples": 3,
|
| 30 |
+
"mean_cer": 0.16849816849816848,
|
| 31 |
+
"mean_digit_cer": 0.0,
|
| 32 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 33 |
+
"mean_table_score": null,
|
| 34 |
+
"num_digit_samples": 3,
|
| 35 |
+
"num_cjk_samples": 3,
|
| 36 |
+
"num_table_samples": 0
|
| 37 |
+
},
|
| 38 |
+
"english_document": {
|
| 39 |
+
"num_samples": 2,
|
| 40 |
+
"mean_cer": 0.43465812665466064,
|
| 41 |
+
"mean_digit_cer": 0.03268053855569156,
|
| 42 |
+
"mean_cjk_cer": null,
|
| 43 |
+
"mean_table_score": null,
|
| 44 |
+
"num_digit_samples": 2,
|
| 45 |
+
"num_cjk_samples": 0,
|
| 46 |
+
"num_table_samples": 0
|
| 47 |
+
},
|
| 48 |
+
"low_quality_scan": {
|
| 49 |
+
"num_samples": 2,
|
| 50 |
+
"mean_cer": 0.0,
|
| 51 |
+
"mean_digit_cer": 0.0,
|
| 52 |
+
"mean_cjk_cer": null,
|
| 53 |
+
"mean_table_score": null,
|
| 54 |
+
"num_digit_samples": 2,
|
| 55 |
+
"num_cjk_samples": 0,
|
| 56 |
+
"num_table_samples": 0
|
| 57 |
+
},
|
| 58 |
+
"table_numeric": {
|
| 59 |
+
"num_samples": 3,
|
| 60 |
+
"mean_cer": 0.1753986332574032,
|
| 61 |
+
"mean_digit_cer": 0.1875,
|
| 62 |
+
"mean_cjk_cer": null,
|
| 63 |
+
"mean_table_score": 1.0,
|
| 64 |
+
"num_digit_samples": 3,
|
| 65 |
+
"num_cjk_samples": 0,
|
| 66 |
+
"num_table_samples": 3
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"per_file": [
|
| 70 |
+
{
|
| 71 |
+
"file": "0001.png",
|
| 72 |
+
"category": "english_document",
|
| 73 |
+
"cer": 0.08904719501335707,
|
| 74 |
+
"wer": 0.24663677130044842,
|
| 75 |
+
"digit_cer": 0.023255813953488372,
|
| 76 |
+
"cjk_cer": 0.0,
|
| 77 |
+
"repetition_rate": 0.0,
|
| 78 |
+
"table_score": null,
|
| 79 |
+
"elapsed_seconds": 23.43824337499973,
|
| 80 |
+
"ref_length": 1345,
|
| 81 |
+
"hyp_length": 1259,
|
| 82 |
+
"ref_digit_count": 86,
|
| 83 |
+
"ref_cjk_count": 0
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"file": "0002.png",
|
| 87 |
+
"category": "english_document",
|
| 88 |
+
"cer": 0.7802690582959642,
|
| 89 |
+
"wer": 0.41916167664670656,
|
| 90 |
+
"digit_cer": 0.042105263157894736,
|
| 91 |
+
"cjk_cer": 0.0,
|
| 92 |
+
"repetition_rate": 0.0,
|
| 93 |
+
"table_score": null,
|
| 94 |
+
"elapsed_seconds": 34.438602624999476,
|
| 95 |
+
"ref_length": 835,
|
| 96 |
+
"hyp_length": 1285,
|
| 97 |
+
"ref_digit_count": 95,
|
| 98 |
+
"ref_cjk_count": 0
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"file": "0003.png",
|
| 102 |
+
"category": "table_numeric",
|
| 103 |
+
"cer": 0.17539863325740318,
|
| 104 |
+
"wer": 1.5,
|
| 105 |
+
"digit_cer": 0.1875,
|
| 106 |
+
"cjk_cer": 0.0,
|
| 107 |
+
"repetition_rate": 0.0,
|
| 108 |
+
"table_score": 1.0,
|
| 109 |
+
"elapsed_seconds": 13.494044040999142,
|
| 110 |
+
"ref_length": 446,
|
| 111 |
+
"hyp_length": 519,
|
| 112 |
+
"ref_digit_count": 80,
|
| 113 |
+
"ref_cjk_count": 0
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"file": "0004.png",
|
| 117 |
+
"category": "table_numeric",
|
| 118 |
+
"cer": 0.17539863325740318,
|
| 119 |
+
"wer": 1.5,
|
| 120 |
+
"digit_cer": 0.1875,
|
| 121 |
+
"cjk_cer": 0.0,
|
| 122 |
+
"repetition_rate": 0.0,
|
| 123 |
+
"table_score": 1.0,
|
| 124 |
+
"elapsed_seconds": 12.677681540997582,
|
| 125 |
+
"ref_length": 446,
|
| 126 |
+
"hyp_length": 519,
|
| 127 |
+
"ref_digit_count": 80,
|
| 128 |
+
"ref_cjk_count": 0
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"file": "0005.png",
|
| 132 |
+
"category": "table_numeric",
|
| 133 |
+
"cer": 0.17539863325740318,
|
| 134 |
+
"wer": 1.5,
|
| 135 |
+
"digit_cer": 0.1875,
|
| 136 |
+
"cjk_cer": 0.0,
|
| 137 |
+
"repetition_rate": 0.0,
|
| 138 |
+
"table_score": 1.0,
|
| 139 |
+
"elapsed_seconds": 12.359245291998377,
|
| 140 |
+
"ref_length": 446,
|
| 141 |
+
"hyp_length": 519,
|
| 142 |
+
"ref_digit_count": 80,
|
| 143 |
+
"ref_cjk_count": 0
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"file": "0006.png",
|
| 147 |
+
"category": "cjk_document",
|
| 148 |
+
"cer": 0.0,
|
| 149 |
+
"wer": 0.0,
|
| 150 |
+
"digit_cer": 0.0,
|
| 151 |
+
"cjk_cer": 0.0,
|
| 152 |
+
"repetition_rate": 0.0,
|
| 153 |
+
"table_score": null,
|
| 154 |
+
"elapsed_seconds": 7.067715750003117,
|
| 155 |
+
"ref_length": 98,
|
| 156 |
+
"hyp_length": 98,
|
| 157 |
+
"ref_digit_count": 30,
|
| 158 |
+
"ref_cjk_count": 41
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"file": "0007.png",
|
| 162 |
+
"category": "cjk_document",
|
| 163 |
+
"cer": 0.0,
|
| 164 |
+
"wer": 0.0,
|
| 165 |
+
"digit_cer": 0.0,
|
| 166 |
+
"cjk_cer": 0.0,
|
| 167 |
+
"repetition_rate": 0.0,
|
| 168 |
+
"table_score": null,
|
| 169 |
+
"elapsed_seconds": 7.551363666992984,
|
| 170 |
+
"ref_length": 100,
|
| 171 |
+
"hyp_length": 100,
|
| 172 |
+
"ref_digit_count": 30,
|
| 173 |
+
"ref_cjk_count": 42
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"file": "0008.png",
|
| 177 |
+
"category": "cjk_document",
|
| 178 |
+
"cer": 0.5054945054945055,
|
| 179 |
+
"wer": 1.2272727272727273,
|
| 180 |
+
"digit_cer": 0.0,
|
| 181 |
+
"cjk_cer": 1.0,
|
| 182 |
+
"repetition_rate": 0.0,
|
| 183 |
+
"table_score": null,
|
| 184 |
+
"elapsed_seconds": 6.661412207991816,
|
| 185 |
+
"ref_length": 112,
|
| 186 |
+
"hyp_length": 118,
|
| 187 |
+
"ref_digit_count": 30,
|
| 188 |
+
"ref_cjk_count": 44
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"file": "0009.png",
|
| 192 |
+
"category": "low_quality_scan",
|
| 193 |
+
"cer": 0.0,
|
| 194 |
+
"wer": 0.0,
|
| 195 |
+
"digit_cer": 0.0,
|
| 196 |
+
"cjk_cer": 0.0,
|
| 197 |
+
"repetition_rate": 0.0,
|
| 198 |
+
"table_score": null,
|
| 199 |
+
"elapsed_seconds": 6.571904916010681,
|
| 200 |
+
"ref_length": 120,
|
| 201 |
+
"hyp_length": 120,
|
| 202 |
+
"ref_digit_count": 29,
|
| 203 |
+
"ref_cjk_count": 0
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"file": "0010.png",
|
| 207 |
+
"category": "low_quality_scan",
|
| 208 |
+
"cer": 0.0,
|
| 209 |
+
"wer": 0.0,
|
| 210 |
+
"digit_cer": 0.0,
|
| 211 |
+
"cjk_cer": 0.0,
|
| 212 |
+
"repetition_rate": 0.0,
|
| 213 |
+
"table_score": null,
|
| 214 |
+
"elapsed_seconds": 6.526005207997514,
|
| 215 |
+
"ref_length": 120,
|
| 216 |
+
"hyp_length": 120,
|
| 217 |
+
"ref_digit_count": 29,
|
| 218 |
+
"ref_cjk_count": 0
|
| 219 |
+
}
|
| 220 |
+
]
|
| 221 |
+
}
|
release/calibration_bfloat16_performance.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16",
|
| 3 |
+
"served_revision": null,
|
| 4 |
+
"image_path": "test_invoice.png",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 256,
|
| 7 |
+
"system": {
|
| 8 |
+
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 9 |
+
"processor": "arm",
|
| 10 |
+
"python_version": "3.14.6",
|
| 11 |
+
"machine": "arm64",
|
| 12 |
+
"mlx_version": "0.32.0",
|
| 13 |
+
"mlx_vlm_version": "0.6.6",
|
| 14 |
+
"chip": "Apple M3 Max",
|
| 15 |
+
"total_memory_gb": 128.0
|
| 16 |
+
},
|
| 17 |
+
"model_load_time_seconds": 0.97142708300089,
|
| 18 |
+
"memory_after_load_mb": 4240.671875,
|
| 19 |
+
"num_warmup": 1,
|
| 20 |
+
"num_runs": 3,
|
| 21 |
+
"mean_tps": 38.770309481806514,
|
| 22 |
+
"std_tps": 0.6887167709523151,
|
| 23 |
+
"mean_elapsed_seconds": 8.391549889337815,
|
| 24 |
+
"mean_peak_memory_mb": 5420.697071,
|
| 25 |
+
"runs": [
|
| 26 |
+
{
|
| 27 |
+
"run": 1,
|
| 28 |
+
"elapsed_seconds": 8.60731708399544,
|
| 29 |
+
"tokens_generated": 256,
|
| 30 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 31 |
+
"tokens_per_second": 38.03899778227293,
|
| 32 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 33 |
+
"peak_memory_mb": 5420.521431,
|
| 34 |
+
"memory_delta_mb": 1337.1867550000002,
|
| 35 |
+
"process_peak_rss_mb": 4253.21875,
|
| 36 |
+
"prompt_tokens": 697,
|
| 37 |
+
"prompt_tokens_per_second": 380.356298056712,
|
| 38 |
+
"finish_reason": "length"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"run": 2,
|
| 42 |
+
"elapsed_seconds": 8.399659084010636,
|
| 43 |
+
"tokens_generated": 256,
|
| 44 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 45 |
+
"tokens_per_second": 38.57884453275097,
|
| 46 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 47 |
+
"peak_memory_mb": 5420.784891,
|
| 48 |
+
"memory_delta_mb": 1337.188071,
|
| 49 |
+
"process_peak_rss_mb": 4260.28125,
|
| 50 |
+
"prompt_tokens": 697,
|
| 51 |
+
"prompt_tokens_per_second": 401.47375057984203,
|
| 52 |
+
"finish_reason": "length"
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"run": 3,
|
| 56 |
+
"elapsed_seconds": 8.167673500007368,
|
| 57 |
+
"tokens_generated": 256,
|
| 58 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 59 |
+
"tokens_per_second": 39.693086130395656,
|
| 60 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 61 |
+
"peak_memory_mb": 5420.784891,
|
| 62 |
+
"memory_delta_mb": 1337.188071,
|
| 63 |
+
"process_peak_rss_mb": 4260.46875,
|
| 64 |
+
"prompt_tokens": 697,
|
| 65 |
+
"prompt_tokens_per_second": 411.1295077060184,
|
| 66 |
+
"finish_reason": "length"
|
| 67 |
+
}
|
| 68 |
+
]
|
| 69 |
+
}
|
release/calibration_mxfp8_accuracy.json
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8",
|
| 3 |
+
"served_revision": null,
|
| 4 |
+
"backend": "mlx",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 1024,
|
| 7 |
+
"profile": "accurate",
|
| 8 |
+
"generation_settings": {
|
| 9 |
+
"temperature": 0.0,
|
| 10 |
+
"top_p": 1.0,
|
| 11 |
+
"repetition_penalty": 1.0,
|
| 12 |
+
"no_repeat_ngram_size": 35,
|
| 13 |
+
"ngram_window": 128
|
| 14 |
+
},
|
| 15 |
+
"num_images": 10,
|
| 16 |
+
"num_samples": 10,
|
| 17 |
+
"total_time_seconds": 120.2266979579872,
|
| 18 |
+
"mean_cer": 0.21607021200652196,
|
| 19 |
+
"mean_wer": 0.7193288921545413,
|
| 20 |
+
"num_digit_samples": 10,
|
| 21 |
+
"num_cjk_samples": 3,
|
| 22 |
+
"mean_digit_cer": 0.1052422480620155,
|
| 23 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 24 |
+
"num_table_samples": 3,
|
| 25 |
+
"mean_table_score": 1.0,
|
| 26 |
+
"mean_repetition_rate": 0.0,
|
| 27 |
+
"by_category": {
|
| 28 |
+
"cjk_document": {
|
| 29 |
+
"num_samples": 3,
|
| 30 |
+
"mean_cer": 0.2490842490842491,
|
| 31 |
+
"mean_digit_cer": 0.08888888888888889,
|
| 32 |
+
"mean_cjk_cer": 0.3333333333333333,
|
| 33 |
+
"mean_table_score": null,
|
| 34 |
+
"num_digit_samples": 3,
|
| 35 |
+
"num_cjk_samples": 3,
|
| 36 |
+
"num_table_samples": 0
|
| 37 |
+
},
|
| 38 |
+
"english_document": {
|
| 39 |
+
"num_samples": 2,
|
| 40 |
+
"mean_cer": 0.44362673652013146,
|
| 41 |
+
"mean_digit_cer": 0.11162790697674418,
|
| 42 |
+
"mean_cjk_cer": null,
|
| 43 |
+
"mean_table_score": null,
|
| 44 |
+
"num_digit_samples": 2,
|
| 45 |
+
"num_cjk_samples": 0,
|
| 46 |
+
"num_table_samples": 0
|
| 47 |
+
},
|
| 48 |
+
"low_quality_scan": {
|
| 49 |
+
"num_samples": 2,
|
| 50 |
+
"mean_cer": 0.0,
|
| 51 |
+
"mean_digit_cer": 0.0,
|
| 52 |
+
"mean_cjk_cer": null,
|
| 53 |
+
"mean_table_score": null,
|
| 54 |
+
"num_digit_samples": 2,
|
| 55 |
+
"num_cjk_samples": 0,
|
| 56 |
+
"num_table_samples": 0
|
| 57 |
+
},
|
| 58 |
+
"table_numeric": {
|
| 59 |
+
"num_samples": 3,
|
| 60 |
+
"mean_cer": 0.1753986332574032,
|
| 61 |
+
"mean_digit_cer": 0.1875,
|
| 62 |
+
"mean_cjk_cer": null,
|
| 63 |
+
"mean_table_score": 1.0,
|
| 64 |
+
"num_digit_samples": 3,
|
| 65 |
+
"num_cjk_samples": 0,
|
| 66 |
+
"num_table_samples": 3
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"per_file": [
|
| 70 |
+
{
|
| 71 |
+
"file": "0001.png",
|
| 72 |
+
"category": "english_document",
|
| 73 |
+
"cer": 0.08904719501335707,
|
| 74 |
+
"wer": 0.24663677130044842,
|
| 75 |
+
"digit_cer": 0.023255813953488372,
|
| 76 |
+
"cjk_cer": 0.0,
|
| 77 |
+
"repetition_rate": 0.0,
|
| 78 |
+
"table_score": null,
|
| 79 |
+
"elapsed_seconds": 21.754243709001457,
|
| 80 |
+
"ref_length": 1345,
|
| 81 |
+
"hyp_length": 1259,
|
| 82 |
+
"ref_digit_count": 86,
|
| 83 |
+
"ref_cjk_count": 0
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"file": "0002.png",
|
| 87 |
+
"category": "english_document",
|
| 88 |
+
"cer": 0.7982062780269058,
|
| 89 |
+
"wer": 0.40119760479041916,
|
| 90 |
+
"digit_cer": 0.2,
|
| 91 |
+
"cjk_cer": 1.0,
|
| 92 |
+
"repetition_rate": 0.0,
|
| 93 |
+
"table_score": null,
|
| 94 |
+
"elapsed_seconds": 30.940298208006425,
|
| 95 |
+
"ref_length": 835,
|
| 96 |
+
"hyp_length": 1293,
|
| 97 |
+
"ref_digit_count": 95,
|
| 98 |
+
"ref_cjk_count": 0
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"file": "0003.png",
|
| 102 |
+
"category": "table_numeric",
|
| 103 |
+
"cer": 0.17539863325740318,
|
| 104 |
+
"wer": 1.5,
|
| 105 |
+
"digit_cer": 0.1875,
|
| 106 |
+
"cjk_cer": 0.0,
|
| 107 |
+
"repetition_rate": 0.0,
|
| 108 |
+
"table_score": 1.0,
|
| 109 |
+
"elapsed_seconds": 11.853719249993446,
|
| 110 |
+
"ref_length": 446,
|
| 111 |
+
"hyp_length": 519,
|
| 112 |
+
"ref_digit_count": 80,
|
| 113 |
+
"ref_cjk_count": 0
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"file": "0004.png",
|
| 117 |
+
"category": "table_numeric",
|
| 118 |
+
"cer": 0.17539863325740318,
|
| 119 |
+
"wer": 1.5,
|
| 120 |
+
"digit_cer": 0.1875,
|
| 121 |
+
"cjk_cer": 0.0,
|
| 122 |
+
"repetition_rate": 0.0,
|
| 123 |
+
"table_score": 1.0,
|
| 124 |
+
"elapsed_seconds": 11.799329292000039,
|
| 125 |
+
"ref_length": 446,
|
| 126 |
+
"hyp_length": 519,
|
| 127 |
+
"ref_digit_count": 80,
|
| 128 |
+
"ref_cjk_count": 0
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"file": "0005.png",
|
| 132 |
+
"category": "table_numeric",
|
| 133 |
+
"cer": 0.17539863325740318,
|
| 134 |
+
"wer": 1.5,
|
| 135 |
+
"digit_cer": 0.1875,
|
| 136 |
+
"cjk_cer": 0.0,
|
| 137 |
+
"repetition_rate": 0.0,
|
| 138 |
+
"table_score": 1.0,
|
| 139 |
+
"elapsed_seconds": 11.05323970799509,
|
| 140 |
+
"ref_length": 446,
|
| 141 |
+
"hyp_length": 519,
|
| 142 |
+
"ref_digit_count": 80,
|
| 143 |
+
"ref_cjk_count": 0
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"file": "0006.png",
|
| 147 |
+
"category": "cjk_document",
|
| 148 |
+
"cer": 0.0,
|
| 149 |
+
"wer": 0.0,
|
| 150 |
+
"digit_cer": 0.0,
|
| 151 |
+
"cjk_cer": 0.0,
|
| 152 |
+
"repetition_rate": 0.0,
|
| 153 |
+
"table_score": null,
|
| 154 |
+
"elapsed_seconds": 6.7778485829912825,
|
| 155 |
+
"ref_length": 98,
|
| 156 |
+
"hyp_length": 98,
|
| 157 |
+
"ref_digit_count": 30,
|
| 158 |
+
"ref_cjk_count": 41
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"file": "0007.png",
|
| 162 |
+
"category": "cjk_document",
|
| 163 |
+
"cer": 0.0,
|
| 164 |
+
"wer": 0.0,
|
| 165 |
+
"digit_cer": 0.0,
|
| 166 |
+
"cjk_cer": 0.0,
|
| 167 |
+
"repetition_rate": 0.0,
|
| 168 |
+
"table_score": null,
|
| 169 |
+
"elapsed_seconds": 6.7498448330006795,
|
| 170 |
+
"ref_length": 100,
|
| 171 |
+
"hyp_length": 100,
|
| 172 |
+
"ref_digit_count": 30,
|
| 173 |
+
"ref_cjk_count": 42
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"file": "0008.png",
|
| 177 |
+
"category": "cjk_document",
|
| 178 |
+
"cer": 0.7472527472527473,
|
| 179 |
+
"wer": 2.0454545454545454,
|
| 180 |
+
"digit_cer": 0.26666666666666666,
|
| 181 |
+
"cjk_cer": 1.0,
|
| 182 |
+
"repetition_rate": 0.0,
|
| 183 |
+
"table_score": null,
|
| 184 |
+
"elapsed_seconds": 6.956620541997836,
|
| 185 |
+
"ref_length": 112,
|
| 186 |
+
"hyp_length": 146,
|
| 187 |
+
"ref_digit_count": 30,
|
| 188 |
+
"ref_cjk_count": 44
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"file": "0009.png",
|
| 192 |
+
"category": "low_quality_scan",
|
| 193 |
+
"cer": 0.0,
|
| 194 |
+
"wer": 0.0,
|
| 195 |
+
"digit_cer": 0.0,
|
| 196 |
+
"cjk_cer": 0.0,
|
| 197 |
+
"repetition_rate": 0.0,
|
| 198 |
+
"table_score": null,
|
| 199 |
+
"elapsed_seconds": 6.085180042005959,
|
| 200 |
+
"ref_length": 120,
|
| 201 |
+
"hyp_length": 120,
|
| 202 |
+
"ref_digit_count": 29,
|
| 203 |
+
"ref_cjk_count": 0
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
"file": "0010.png",
|
| 207 |
+
"category": "low_quality_scan",
|
| 208 |
+
"cer": 0.0,
|
| 209 |
+
"wer": 0.0,
|
| 210 |
+
"digit_cer": 0.0,
|
| 211 |
+
"cjk_cer": 0.0,
|
| 212 |
+
"repetition_rate": 0.0,
|
| 213 |
+
"table_score": null,
|
| 214 |
+
"elapsed_seconds": 6.256373790994985,
|
| 215 |
+
"ref_length": 120,
|
| 216 |
+
"hyp_length": 120,
|
| 217 |
+
"ref_digit_count": 29,
|
| 218 |
+
"ref_cjk_count": 0
|
| 219 |
+
}
|
| 220 |
+
]
|
| 221 |
+
}
|
release/calibration_mxfp8_performance.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8",
|
| 3 |
+
"served_revision": null,
|
| 4 |
+
"image_path": "test_invoice.png",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 256,
|
| 7 |
+
"system": {
|
| 8 |
+
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 9 |
+
"processor": "arm",
|
| 10 |
+
"python_version": "3.14.6",
|
| 11 |
+
"machine": "arm64",
|
| 12 |
+
"mlx_version": "0.32.0",
|
| 13 |
+
"mlx_vlm_version": "0.6.6",
|
| 14 |
+
"chip": "Apple M3 Max",
|
| 15 |
+
"total_memory_gb": 128.0
|
| 16 |
+
},
|
| 17 |
+
"model_load_time_seconds": 1.0123953330039512,
|
| 18 |
+
"memory_after_load_mb": 4115.359375,
|
| 19 |
+
"num_warmup": 1,
|
| 20 |
+
"num_runs": 3,
|
| 21 |
+
"mean_tps": 41.326731942543184,
|
| 22 |
+
"std_tps": 1.2149507723608595,
|
| 23 |
+
"mean_elapsed_seconds": 7.814036472002044,
|
| 24 |
+
"mean_peak_memory_mb": 5260.396014999999,
|
| 25 |
+
"runs": [
|
| 26 |
+
{
|
| 27 |
+
"run": 1,
|
| 28 |
+
"elapsed_seconds": 7.896335166005883,
|
| 29 |
+
"tokens_generated": 256,
|
| 30 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 31 |
+
"tokens_per_second": 41.34150964421114,
|
| 32 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 33 |
+
"peak_memory_mb": 5260.220375,
|
| 34 |
+
"memory_delta_mb": 1337.1867549999997,
|
| 35 |
+
"process_peak_rss_mb": 4129.09375,
|
| 36 |
+
"prompt_tokens": 697,
|
| 37 |
+
"prompt_tokens_per_second": 421.22509117788985,
|
| 38 |
+
"finish_reason": "length"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"run": 2,
|
| 42 |
+
"elapsed_seconds": 7.41755587499938,
|
| 43 |
+
"tokens_generated": 256,
|
| 44 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 45 |
+
"tokens_per_second": 42.80729278290862,
|
| 46 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 47 |
+
"peak_memory_mb": 5260.483835,
|
| 48 |
+
"memory_delta_mb": 1337.188071,
|
| 49 |
+
"process_peak_rss_mb": 4137.328125,
|
| 50 |
+
"prompt_tokens": 697,
|
| 51 |
+
"prompt_tokens_per_second": 496.01062497025777,
|
| 52 |
+
"finish_reason": "length"
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"run": 3,
|
| 56 |
+
"elapsed_seconds": 8.12821837500087,
|
| 57 |
+
"tokens_generated": 256,
|
| 58 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 59 |
+
"tokens_per_second": 39.83139340050979,
|
| 60 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 61 |
+
"peak_memory_mb": 5260.483835,
|
| 62 |
+
"memory_delta_mb": 1337.188071,
|
| 63 |
+
"process_peak_rss_mb": 4137.578125,
|
| 64 |
+
"prompt_tokens": 697,
|
| 65 |
+
"prompt_tokens_per_second": 415.31823155489394,
|
| 66 |
+
"finish_reason": "length"
|
| 67 |
+
}
|
| 68 |
+
]
|
| 69 |
+
}
|
release/calibration_reference_performance.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 3 |
+
"served_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 4 |
+
"image_path": "test_invoice.png",
|
| 5 |
+
"prompt": "<image>document parsing.",
|
| 6 |
+
"max_tokens": 256,
|
| 7 |
+
"system": {
|
| 8 |
+
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 9 |
+
"processor": "arm",
|
| 10 |
+
"python_version": "3.14.6",
|
| 11 |
+
"machine": "arm64",
|
| 12 |
+
"mlx_version": "0.32.0",
|
| 13 |
+
"mlx_vlm_version": "0.6.6",
|
| 14 |
+
"chip": "Apple M3 Max",
|
| 15 |
+
"total_memory_gb": 128.0
|
| 16 |
+
},
|
| 17 |
+
"model_load_time_seconds": 5.673565958000836,
|
| 18 |
+
"memory_after_load_mb": 3994.578125,
|
| 19 |
+
"num_warmup": 1,
|
| 20 |
+
"num_runs": 3,
|
| 21 |
+
"mean_tps": 40.402663236114016,
|
| 22 |
+
"std_tps": 0.3014552552023151,
|
| 23 |
+
"mean_elapsed_seconds": 8.03746888867075,
|
| 24 |
+
"mean_peak_memory_mb": 5165.991419999999,
|
| 25 |
+
"runs": [
|
| 26 |
+
{
|
| 27 |
+
"run": 1,
|
| 28 |
+
"elapsed_seconds": 8.067712333009695,
|
| 29 |
+
"tokens_generated": 256,
|
| 30 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 31 |
+
"tokens_per_second": 40.079502760011906,
|
| 32 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 33 |
+
"peak_memory_mb": 5165.81578,
|
| 34 |
+
"memory_delta_mb": 1337.186768,
|
| 35 |
+
"process_peak_rss_mb": 4016.75,
|
| 36 |
+
"prompt_tokens": 697,
|
| 37 |
+
"prompt_tokens_per_second": 422.0660830294658,
|
| 38 |
+
"finish_reason": "length"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"run": 2,
|
| 42 |
+
"elapsed_seconds": 8.119986582998536,
|
| 43 |
+
"tokens_generated": 256,
|
| 44 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 45 |
+
"tokens_per_second": 40.323435695329465,
|
| 46 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 47 |
+
"peak_memory_mb": 5166.07924,
|
| 48 |
+
"memory_delta_mb": 1337.188084,
|
| 49 |
+
"process_peak_rss_mb": 4025.171875,
|
| 50 |
+
"prompt_tokens": 697,
|
| 51 |
+
"prompt_tokens_per_second": 399.62688128036274,
|
| 52 |
+
"finish_reason": "length"
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"run": 3,
|
| 56 |
+
"elapsed_seconds": 7.924707750004018,
|
| 57 |
+
"tokens_generated": 256,
|
| 58 |
+
"tokens_generated_source": "mlx-vlm token count",
|
| 59 |
+
"tokens_per_second": 40.80505125300066,
|
| 60 |
+
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 61 |
+
"peak_memory_mb": 5166.07924,
|
| 62 |
+
"memory_delta_mb": 1337.188084,
|
| 63 |
+
"process_peak_rss_mb": 4025.484375,
|
| 64 |
+
"prompt_tokens": 697,
|
| 65 |
+
"prompt_tokens_per_second": 429.46142333454605,
|
| 66 |
+
"finish_reason": "length"
|
| 67 |
+
}
|
| 68 |
+
]
|
| 69 |
+
}
|
release/calibration_results.json
CHANGED
|
@@ -1,38 +1,40 @@
|
|
| 1 |
{
|
| 2 |
-
"schema_version":
|
| 3 |
-
"created_at": "2026-07-
|
| 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.
|
| 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":
|
| 19 |
"checks": {
|
| 20 |
"cer": true,
|
| 21 |
"digit_cer": true,
|
| 22 |
"table_score": true,
|
| 23 |
-
"throughput":
|
| 24 |
},
|
| 25 |
"metrics": {
|
| 26 |
-
"mean_cer": 0.
|
| 27 |
-
"mean_digit_cer": 0.
|
| 28 |
"mean_table_score": 1.0,
|
| 29 |
-
"mean_tps":
|
| 30 |
},
|
| 31 |
"deltas": {
|
| 32 |
-
"cer_vs_bf16": -0.
|
| 33 |
-
"digit_cer_vs_bf16": -0.
|
| 34 |
"table_degradation_vs_bf16": 0.0,
|
| 35 |
-
"tps_ratio_vs_reference": 0.
|
| 36 |
}
|
| 37 |
},
|
| 38 |
{
|
|
@@ -40,22 +42,22 @@
|
|
| 40 |
"precision": "mxfp8",
|
| 41 |
"passed": false,
|
| 42 |
"checks": {
|
| 43 |
-
"cer":
|
| 44 |
-
"digit_cer":
|
| 45 |
"table_score": true,
|
| 46 |
"throughput": true
|
| 47 |
},
|
| 48 |
"metrics": {
|
| 49 |
-
"mean_cer": 0.
|
| 50 |
-
"mean_digit_cer": 0.
|
| 51 |
"mean_table_score": 1.0,
|
| 52 |
-
"mean_tps":
|
| 53 |
},
|
| 54 |
"deltas": {
|
| 55 |
-
"cer_vs_bf16": 0.
|
| 56 |
-
"digit_cer_vs_bf16":
|
| 57 |
"table_degradation_vs_bf16": 0.0,
|
| 58 |
-
"tps_ratio_vs_reference":
|
| 59 |
}
|
| 60 |
},
|
| 61 |
{
|
|
@@ -69,16 +71,16 @@
|
|
| 69 |
"throughput": true
|
| 70 |
},
|
| 71 |
"metrics": {
|
| 72 |
-
"mean_cer": 0.
|
| 73 |
-
"mean_digit_cer": 0.
|
| 74 |
"mean_table_score": 1.0,
|
| 75 |
-
"mean_tps":
|
| 76 |
},
|
| 77 |
"deltas": {
|
| 78 |
-
"cer_vs_bf16": -0.
|
| 79 |
-
"digit_cer_vs_bf16":
|
| 80 |
"table_degradation_vs_bf16": 0.0,
|
| 81 |
-
"tps_ratio_vs_reference": 0.
|
| 82 |
}
|
| 83 |
}
|
| 84 |
],
|
|
@@ -88,5 +90,164 @@
|
|
| 88 |
},
|
| 89 |
"precision_overrides": {
|
| 90 |
"language_model.lm_head": "affine8"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"schema_version": 3,
|
| 3 |
+
"created_at": "2026-07-21T05:26:12.430585+00:00",
|
| 4 |
"target_pattern": "language_model.lm_head",
|
| 5 |
"selection_policy": "fastest candidate passing existing quality and throughput limits",
|
| 6 |
"thresholds": {
|
| 7 |
+
"max_cer_delta_vs_bf16": 0.015,
|
| 8 |
"max_cer_delta_vs_reference": 0.005,
|
| 9 |
"max_digit_cer_delta_vs_bf16": 0.01,
|
| 10 |
"max_table_score_degradation_vs_bf16": 0.01,
|
| 11 |
"min_tps_ratio_vs_reference": 0.9,
|
| 12 |
+
"max_weight_size_gb": 4.5,
|
| 13 |
+
"min_rswa_tokens": 8192,
|
| 14 |
+
"max_rswa_repetition_rate": 0.25
|
| 15 |
},
|
| 16 |
"experiments": [
|
| 17 |
{
|
| 18 |
"label": "bf16-head",
|
| 19 |
"precision": "bfloat16",
|
| 20 |
+
"passed": true,
|
| 21 |
"checks": {
|
| 22 |
"cer": true,
|
| 23 |
"digit_cer": true,
|
| 24 |
"table_score": true,
|
| 25 |
+
"throughput": true
|
| 26 |
},
|
| 27 |
"metrics": {
|
| 28 |
+
"mean_cer": 0.19010066585760363,
|
| 29 |
+
"mean_digit_cer": 0.06278610771113831,
|
| 30 |
"mean_table_score": 1.0,
|
| 31 |
+
"mean_tps": 38.770309481806514
|
| 32 |
},
|
| 33 |
"deltas": {
|
| 34 |
+
"cer_vs_bf16": -0.012208451030632145,
|
| 35 |
+
"digit_cer_vs_bf16": -0.00958384332925337,
|
| 36 |
"table_degradation_vs_bf16": 0.0,
|
| 37 |
+
"tps_ratio_vs_reference": 0.9595978674779929
|
| 38 |
}
|
| 39 |
},
|
| 40 |
{
|
|
|
|
| 42 |
"precision": "mxfp8",
|
| 43 |
"passed": false,
|
| 44 |
"checks": {
|
| 45 |
+
"cer": true,
|
| 46 |
+
"digit_cer": false,
|
| 47 |
"table_score": true,
|
| 48 |
"throughput": true
|
| 49 |
},
|
| 50 |
"metrics": {
|
| 51 |
+
"mean_cer": 0.21607021200652196,
|
| 52 |
+
"mean_digit_cer": 0.1052422480620155,
|
| 53 |
"mean_table_score": 1.0,
|
| 54 |
+
"mean_tps": 41.326731942543184
|
| 55 |
},
|
| 56 |
"deltas": {
|
| 57 |
+
"cer_vs_bf16": 0.013761095118286187,
|
| 58 |
+
"digit_cer_vs_bf16": 0.03287229702162382,
|
| 59 |
"table_degradation_vs_bf16": 0.0,
|
| 60 |
+
"tps_ratio_vs_reference": 1.0228714800563738
|
| 61 |
}
|
| 62 |
},
|
| 63 |
{
|
|
|
|
| 71 |
"throughput": true
|
| 72 |
},
|
| 73 |
"metrics": {
|
| 74 |
+
"mean_cer": 0.19105830313811087,
|
| 75 |
+
"mean_digit_cer": 0.07857558139534884,
|
| 76 |
"mean_table_score": 1.0,
|
| 77 |
+
"mean_tps": 39.58614991640946
|
| 78 |
},
|
| 79 |
"deltas": {
|
| 80 |
+
"cer_vs_bf16": -0.011250813750124905,
|
| 81 |
+
"digit_cer_vs_bf16": 0.0062056303549571645,
|
| 82 |
"table_degradation_vs_bf16": 0.0,
|
| 83 |
+
"tps_ratio_vs_reference": 0.979790606502031
|
| 84 |
}
|
| 85 |
}
|
| 86 |
],
|
|
|
|
| 90 |
},
|
| 91 |
"precision_overrides": {
|
| 92 |
"language_model.lm_head": "affine8"
|
| 93 |
+
},
|
| 94 |
+
"input_artifacts": {
|
| 95 |
+
"calibration_baseline_accuracy": {
|
| 96 |
+
"filename": "calibration_baseline_accuracy.json",
|
| 97 |
+
"size": 5680,
|
| 98 |
+
"sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
|
| 99 |
+
},
|
| 100 |
+
"calibration_reference_performance": {
|
| 101 |
+
"filename": "calibration_reference_performance.json",
|
| 102 |
+
"size": 2271,
|
| 103 |
+
"sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
|
| 104 |
+
},
|
| 105 |
+
"calibration_bfloat16_accuracy": {
|
| 106 |
+
"filename": "calibration_bfloat16_accuracy.json",
|
| 107 |
+
"size": 5678,
|
| 108 |
+
"sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
|
| 109 |
+
},
|
| 110 |
+
"calibration_bfloat16_performance": {
|
| 111 |
+
"filename": "calibration_bfloat16_performance.json",
|
| 112 |
+
"size": 2244,
|
| 113 |
+
"sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
|
| 114 |
+
},
|
| 115 |
+
"calibration_mxfp8_accuracy": {
|
| 116 |
+
"filename": "calibration_mxfp8_accuracy.json",
|
| 117 |
+
"size": 5689,
|
| 118 |
+
"sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
|
| 119 |
+
},
|
| 120 |
+
"calibration_mxfp8_performance": {
|
| 121 |
+
"filename": "calibration_mxfp8_performance.json",
|
| 122 |
+
"size": 2252,
|
| 123 |
+
"sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
|
| 124 |
+
},
|
| 125 |
+
"calibration_affine8_accuracy": {
|
| 126 |
+
"filename": "calibration_affine8_accuracy.json",
|
| 127 |
+
"size": 5657,
|
| 128 |
+
"sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
|
| 129 |
+
},
|
| 130 |
+
"calibration_affine8_performance": {
|
| 131 |
+
"filename": "calibration_affine8_performance.json",
|
| 132 |
+
"size": 2247,
|
| 133 |
+
"sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"dataset": {
|
| 137 |
+
"name": "calibration",
|
| 138 |
+
"num_samples": 10,
|
| 139 |
+
"num_digit_samples": 10,
|
| 140 |
+
"num_cjk_samples": 3,
|
| 141 |
+
"num_table_samples": 3,
|
| 142 |
+
"category_counts": {
|
| 143 |
+
"cjk_document": 3,
|
| 144 |
+
"english_document": 2,
|
| 145 |
+
"low_quality_scan": 2,
|
| 146 |
+
"table_numeric": 3
|
| 147 |
+
},
|
| 148 |
+
"manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
|
| 149 |
+
"content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 150 |
+
"files": [
|
| 151 |
+
{
|
| 152 |
+
"path": "images/0001.png",
|
| 153 |
+
"size": 243645,
|
| 154 |
+
"sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"path": "ground_truth/0001.txt",
|
| 158 |
+
"size": 1346,
|
| 159 |
+
"sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"path": "images/0002.png",
|
| 163 |
+
"size": 208699,
|
| 164 |
+
"sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"path": "ground_truth/0002.txt",
|
| 168 |
+
"size": 838,
|
| 169 |
+
"sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"path": "images/0003.png",
|
| 173 |
+
"size": 40039,
|
| 174 |
+
"sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"path": "ground_truth/0003.txt",
|
| 178 |
+
"size": 459,
|
| 179 |
+
"sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"path": "images/0004.png",
|
| 183 |
+
"size": 40356,
|
| 184 |
+
"sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"path": "ground_truth/0004.txt",
|
| 188 |
+
"size": 459,
|
| 189 |
+
"sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"path": "images/0005.png",
|
| 193 |
+
"size": 40650,
|
| 194 |
+
"sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"path": "ground_truth/0005.txt",
|
| 198 |
+
"size": 459,
|
| 199 |
+
"sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"path": "images/0006.png",
|
| 203 |
+
"size": 46036,
|
| 204 |
+
"sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"path": "ground_truth/0006.txt",
|
| 208 |
+
"size": 191,
|
| 209 |
+
"sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"path": "images/0007.png",
|
| 213 |
+
"size": 41828,
|
| 214 |
+
"sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"path": "ground_truth/0007.txt",
|
| 218 |
+
"size": 195,
|
| 219 |
+
"sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
"path": "images/0008.png",
|
| 223 |
+
"size": 20975,
|
| 224 |
+
"sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"path": "ground_truth/0008.txt",
|
| 228 |
+
"size": 201,
|
| 229 |
+
"sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"path": "images/0009.png",
|
| 233 |
+
"size": 487313,
|
| 234 |
+
"sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"path": "ground_truth/0009.txt",
|
| 238 |
+
"size": 121,
|
| 239 |
+
"sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"path": "images/0010.png",
|
| 243 |
+
"size": 515820,
|
| 244 |
+
"sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"path": "ground_truth/0010.txt",
|
| 248 |
+
"size": 121,
|
| 249 |
+
"sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
|
| 250 |
+
}
|
| 251 |
+
]
|
| 252 |
}
|
| 253 |
}
|
release/candidate_accuracy.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
|
|
|
|
|
|
| 3 |
"prompt": "<image>document parsing.",
|
| 4 |
"max_tokens": 1024,
|
| 5 |
"profile": "accurate",
|
|
@@ -12,183 +14,237 @@
|
|
| 12 |
},
|
| 13 |
"num_images": 12,
|
| 14 |
"num_samples": 12,
|
| 15 |
-
"total_time_seconds":
|
| 16 |
-
"mean_cer": 0.
|
| 17 |
-
"mean_wer": 0.
|
| 18 |
"num_digit_samples": 12,
|
| 19 |
"num_cjk_samples": 3,
|
| 20 |
-
"mean_digit_cer": 0.
|
| 21 |
-
"mean_cjk_cer": 0.
|
| 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 |
-
"
|
| 29 |
-
"
|
| 30 |
-
"
|
|
|
|
| 31 |
"cjk_cer": 0.0,
|
| 32 |
"repetition_rate": 0.0,
|
| 33 |
"table_score": null,
|
| 34 |
-
"elapsed_seconds":
|
| 35 |
-
"ref_length":
|
| 36 |
-
"hyp_length":
|
| 37 |
-
"ref_digit_count":
|
| 38 |
"ref_cjk_count": 0
|
| 39 |
},
|
| 40 |
{
|
| 41 |
"file": "0002.png",
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
|
|
|
| 45 |
"cjk_cer": 0.0,
|
| 46 |
"repetition_rate": 0.0,
|
| 47 |
"table_score": null,
|
| 48 |
-
"elapsed_seconds":
|
| 49 |
-
"ref_length":
|
| 50 |
-
"hyp_length":
|
| 51 |
-
"ref_digit_count":
|
| 52 |
"ref_cjk_count": 0
|
| 53 |
},
|
| 54 |
{
|
| 55 |
"file": "0003.png",
|
| 56 |
-
"
|
| 57 |
-
"
|
| 58 |
-
"
|
|
|
|
| 59 |
"cjk_cer": 0.0,
|
| 60 |
"repetition_rate": 0.0,
|
| 61 |
"table_score": null,
|
| 62 |
-
"elapsed_seconds":
|
| 63 |
-
"ref_length":
|
| 64 |
-
"hyp_length":
|
| 65 |
-
"ref_digit_count":
|
| 66 |
"ref_cjk_count": 0
|
| 67 |
},
|
| 68 |
{
|
| 69 |
"file": "0004.png",
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"
|
|
|
|
| 73 |
"cjk_cer": 0.0,
|
| 74 |
"repetition_rate": 0.0,
|
| 75 |
"table_score": null,
|
| 76 |
-
"elapsed_seconds":
|
| 77 |
-
"ref_length":
|
| 78 |
-
"hyp_length":
|
| 79 |
-
"ref_digit_count":
|
| 80 |
"ref_cjk_count": 0
|
| 81 |
},
|
| 82 |
{
|
| 83 |
"file": "0005.png",
|
| 84 |
-
"
|
|
|
|
| 85 |
"wer": 1.5,
|
| 86 |
-
"digit_cer": 0.
|
| 87 |
"cjk_cer": 0.0,
|
| 88 |
"repetition_rate": 0.0,
|
| 89 |
"table_score": 1.0,
|
| 90 |
-
"elapsed_seconds":
|
| 91 |
-
"ref_length":
|
| 92 |
-
"hyp_length":
|
| 93 |
-
"ref_digit_count":
|
| 94 |
"ref_cjk_count": 0
|
| 95 |
},
|
| 96 |
{
|
| 97 |
"file": "0006.png",
|
| 98 |
-
"
|
|
|
|
| 99 |
"wer": 1.5,
|
| 100 |
-
"digit_cer": 0.
|
| 101 |
"cjk_cer": 0.0,
|
| 102 |
"repetition_rate": 0.0,
|
| 103 |
"table_score": 1.0,
|
| 104 |
-
"elapsed_seconds":
|
| 105 |
-
"ref_length":
|
| 106 |
-
"hyp_length":
|
| 107 |
-
"ref_digit_count":
|
| 108 |
"ref_cjk_count": 0
|
| 109 |
},
|
| 110 |
{
|
| 111 |
"file": "0007.png",
|
| 112 |
-
"
|
|
|
|
| 113 |
"wer": 1.5,
|
| 114 |
-
"digit_cer": 0.
|
| 115 |
"cjk_cer": 0.0,
|
| 116 |
"repetition_rate": 0.0,
|
| 117 |
"table_score": 1.0,
|
| 118 |
-
"elapsed_seconds":
|
| 119 |
-
"ref_length":
|
| 120 |
-
"hyp_length":
|
| 121 |
-
"ref_digit_count":
|
| 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":
|
| 133 |
-
"ref_length":
|
| 134 |
-
"hyp_length":
|
| 135 |
-
"ref_digit_count":
|
| 136 |
"ref_cjk_count": 41
|
| 137 |
},
|
| 138 |
{
|
| 139 |
"file": "0009.png",
|
| 140 |
-
"
|
| 141 |
-
"
|
|
|
|
| 142 |
"digit_cer": 0.0,
|
| 143 |
-
"cjk_cer": 0.
|
| 144 |
"repetition_rate": 0.0,
|
| 145 |
"table_score": null,
|
| 146 |
-
"elapsed_seconds":
|
| 147 |
-
"ref_length":
|
| 148 |
-
"hyp_length":
|
| 149 |
-
"ref_digit_count":
|
| 150 |
"ref_cjk_count": 42
|
| 151 |
},
|
| 152 |
{
|
| 153 |
"file": "0010.png",
|
| 154 |
-
"
|
| 155 |
-
"
|
| 156 |
-
"
|
|
|
|
| 157 |
"cjk_cer": 1.0,
|
| 158 |
"repetition_rate": 0.0,
|
| 159 |
"table_score": null,
|
| 160 |
-
"elapsed_seconds":
|
| 161 |
-
"ref_length":
|
| 162 |
-
"hyp_length":
|
| 163 |
-
"ref_digit_count":
|
| 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":
|
| 175 |
-
"ref_length":
|
| 176 |
-
"hyp_length":
|
| 177 |
-
"ref_digit_count":
|
| 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":
|
| 189 |
-
"ref_length":
|
| 190 |
-
"hyp_length":
|
| 191 |
-
"ref_digit_count":
|
| 192 |
"ref_cjk_count": 0
|
| 193 |
}
|
| 194 |
]
|
|
|
|
| 1 |
{
|
| 2 |
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 3 |
+
"served_revision": null,
|
| 4 |
+
"backend": "mlx",
|
| 5 |
"prompt": "<image>document parsing.",
|
| 6 |
"max_tokens": 1024,
|
| 7 |
"profile": "accurate",
|
|
|
|
| 14 |
},
|
| 15 |
"num_images": 12,
|
| 16 |
"num_samples": 12,
|
| 17 |
+
"total_time_seconds": 164.14354283601278,
|
| 18 |
+
"mean_cer": 0.21633091868789586,
|
| 19 |
+
"mean_wer": 0.6718835226038614,
|
| 20 |
"num_digit_samples": 12,
|
| 21 |
"num_cjk_samples": 3,
|
| 22 |
+
"mean_digit_cer": 0.11193906322597019,
|
| 23 |
+
"mean_cjk_cer": 0.3412698412698412,
|
| 24 |
"num_table_samples": 3,
|
| 25 |
"mean_table_score": 1.0,
|
| 26 |
"mean_repetition_rate": 0.0,
|
| 27 |
+
"by_category": {
|
| 28 |
+
"cjk_document": {
|
| 29 |
+
"num_samples": 3,
|
| 30 |
+
"mean_cer": 0.2455035601103017,
|
| 31 |
+
"mean_digit_cer": 0.08888888888888889,
|
| 32 |
+
"mean_cjk_cer": 0.3412698412698412,
|
| 33 |
+
"mean_table_score": null,
|
| 34 |
+
"num_digit_samples": 3,
|
| 35 |
+
"num_cjk_samples": 3,
|
| 36 |
+
"num_table_samples": 0
|
| 37 |
+
},
|
| 38 |
+
"english_document": {
|
| 39 |
+
"num_samples": 4,
|
| 40 |
+
"mean_cer": 0.3333161110379089,
|
| 41 |
+
"mean_digit_cer": 0.12852552301124393,
|
| 42 |
+
"mean_cjk_cer": null,
|
| 43 |
+
"mean_table_score": null,
|
| 44 |
+
"num_digit_samples": 4,
|
| 45 |
+
"num_cjk_samples": 0,
|
| 46 |
+
"num_table_samples": 0
|
| 47 |
+
},
|
| 48 |
+
"low_quality_scan": {
|
| 49 |
+
"num_samples": 2,
|
| 50 |
+
"mean_cer": 0.0,
|
| 51 |
+
"mean_digit_cer": 0.0,
|
| 52 |
+
"mean_cjk_cer": null,
|
| 53 |
+
"mean_table_score": null,
|
| 54 |
+
"num_digit_samples": 2,
|
| 55 |
+
"num_cjk_samples": 0,
|
| 56 |
+
"num_table_samples": 0
|
| 57 |
+
},
|
| 58 |
+
"table_numeric": {
|
| 59 |
+
"num_samples": 3,
|
| 60 |
+
"mean_cer": 0.1753986332574032,
|
| 61 |
+
"mean_digit_cer": 0.1875,
|
| 62 |
+
"mean_cjk_cer": null,
|
| 63 |
+
"mean_table_score": 1.0,
|
| 64 |
+
"num_digit_samples": 3,
|
| 65 |
+
"num_cjk_samples": 0,
|
| 66 |
+
"num_table_samples": 3
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
"per_file": [
|
| 70 |
{
|
| 71 |
"file": "0001.png",
|
| 72 |
+
"category": "english_document",
|
| 73 |
+
"cer": 0.3494191242180518,
|
| 74 |
+
"wer": 0.3317757009345794,
|
| 75 |
+
"digit_cer": 0.14814814814814814,
|
| 76 |
"cjk_cer": 0.0,
|
| 77 |
"repetition_rate": 0.0,
|
| 78 |
"table_score": null,
|
| 79 |
+
"elapsed_seconds": 23.19513687500148,
|
| 80 |
+
"ref_length": 1332,
|
| 81 |
+
"hyp_length": 1669,
|
| 82 |
+
"ref_digit_count": 27,
|
| 83 |
"ref_cjk_count": 0
|
| 84 |
},
|
| 85 |
{
|
| 86 |
"file": "0002.png",
|
| 87 |
+
"category": "english_document",
|
| 88 |
+
"cer": 0.37815884476534295,
|
| 89 |
+
"wer": 0.36486486486486486,
|
| 90 |
+
"digit_cer": 0.1016949152542373,
|
| 91 |
"cjk_cer": 0.0,
|
| 92 |
"repetition_rate": 0.0,
|
| 93 |
"table_score": null,
|
| 94 |
+
"elapsed_seconds": 23.631959624995943,
|
| 95 |
+
"ref_length": 1329,
|
| 96 |
+
"hyp_length": 1708,
|
| 97 |
+
"ref_digit_count": 59,
|
| 98 |
"ref_cjk_count": 0
|
| 99 |
},
|
| 100 |
{
|
| 101 |
"file": "0003.png",
|
| 102 |
+
"category": "english_document",
|
| 103 |
+
"cer": 0.5393474088291746,
|
| 104 |
+
"wer": 0.5086206896551724,
|
| 105 |
+
"digit_cer": 0.136986301369863,
|
| 106 |
"cjk_cer": 0.0,
|
| 107 |
"repetition_rate": 0.0,
|
| 108 |
"table_score": null,
|
| 109 |
+
"elapsed_seconds": 25.065509457999724,
|
| 110 |
+
"ref_length": 636,
|
| 111 |
+
"hyp_length": 804,
|
| 112 |
+
"ref_digit_count": 73,
|
| 113 |
"ref_cjk_count": 0
|
| 114 |
},
|
| 115 |
{
|
| 116 |
"file": "0004.png",
|
| 117 |
+
"category": "english_document",
|
| 118 |
+
"cer": 0.06633906633906633,
|
| 119 |
+
"wer": 0.18309859154929578,
|
| 120 |
+
"digit_cer": 0.12727272727272726,
|
| 121 |
"cjk_cer": 0.0,
|
| 122 |
"repetition_rate": 0.0,
|
| 123 |
"table_score": null,
|
| 124 |
+
"elapsed_seconds": 18.39167737500975,
|
| 125 |
+
"ref_length": 477,
|
| 126 |
+
"hyp_length": 484,
|
| 127 |
+
"ref_digit_count": 55,
|
| 128 |
"ref_cjk_count": 0
|
| 129 |
},
|
| 130 |
{
|
| 131 |
"file": "0005.png",
|
| 132 |
+
"category": "table_numeric",
|
| 133 |
+
"cer": 0.17539863325740318,
|
| 134 |
"wer": 1.5,
|
| 135 |
+
"digit_cer": 0.1875,
|
| 136 |
"cjk_cer": 0.0,
|
| 137 |
"repetition_rate": 0.0,
|
| 138 |
"table_score": 1.0,
|
| 139 |
+
"elapsed_seconds": 12.358092667011078,
|
| 140 |
+
"ref_length": 446,
|
| 141 |
+
"hyp_length": 519,
|
| 142 |
+
"ref_digit_count": 80,
|
| 143 |
"ref_cjk_count": 0
|
| 144 |
},
|
| 145 |
{
|
| 146 |
"file": "0006.png",
|
| 147 |
+
"category": "table_numeric",
|
| 148 |
+
"cer": 0.17539863325740318,
|
| 149 |
"wer": 1.5,
|
| 150 |
+
"digit_cer": 0.1875,
|
| 151 |
"cjk_cer": 0.0,
|
| 152 |
"repetition_rate": 0.0,
|
| 153 |
"table_score": 1.0,
|
| 154 |
+
"elapsed_seconds": 12.517006292007864,
|
| 155 |
+
"ref_length": 446,
|
| 156 |
+
"hyp_length": 519,
|
| 157 |
+
"ref_digit_count": 80,
|
| 158 |
"ref_cjk_count": 0
|
| 159 |
},
|
| 160 |
{
|
| 161 |
"file": "0007.png",
|
| 162 |
+
"category": "table_numeric",
|
| 163 |
+
"cer": 0.17539863325740318,
|
| 164 |
"wer": 1.5,
|
| 165 |
+
"digit_cer": 0.1875,
|
| 166 |
"cjk_cer": 0.0,
|
| 167 |
"repetition_rate": 0.0,
|
| 168 |
"table_score": 1.0,
|
| 169 |
+
"elapsed_seconds": 12.23163116700016,
|
| 170 |
+
"ref_length": 446,
|
| 171 |
+
"hyp_length": 519,
|
| 172 |
+
"ref_digit_count": 80,
|
| 173 |
"ref_cjk_count": 0
|
| 174 |
},
|
| 175 |
{
|
| 176 |
"file": "0008.png",
|
| 177 |
+
"category": "cjk_document",
|
| 178 |
"cer": 0.0,
|
| 179 |
"wer": 0.0,
|
| 180 |
"digit_cer": 0.0,
|
| 181 |
"cjk_cer": 0.0,
|
| 182 |
"repetition_rate": 0.0,
|
| 183 |
"table_score": null,
|
| 184 |
+
"elapsed_seconds": 7.415536249987781,
|
| 185 |
+
"ref_length": 98,
|
| 186 |
+
"hyp_length": 98,
|
| 187 |
+
"ref_digit_count": 30,
|
| 188 |
"ref_cjk_count": 41
|
| 189 |
},
|
| 190 |
{
|
| 191 |
"file": "0009.png",
|
| 192 |
+
"category": "cjk_document",
|
| 193 |
+
"cer": 0.011235955056179775,
|
| 194 |
+
"wer": 0.08333333333333333,
|
| 195 |
"digit_cer": 0.0,
|
| 196 |
+
"cjk_cer": 0.023809523809523808,
|
| 197 |
"repetition_rate": 0.0,
|
| 198 |
"table_score": null,
|
| 199 |
+
"elapsed_seconds": 8.263199041990447,
|
| 200 |
+
"ref_length": 100,
|
| 201 |
+
"hyp_length": 100,
|
| 202 |
+
"ref_digit_count": 30,
|
| 203 |
"ref_cjk_count": 42
|
| 204 |
},
|
| 205 |
{
|
| 206 |
"file": "0010.png",
|
| 207 |
+
"category": "cjk_document",
|
| 208 |
+
"cer": 0.7252747252747253,
|
| 209 |
+
"wer": 2.090909090909091,
|
| 210 |
+
"digit_cer": 0.26666666666666666,
|
| 211 |
"cjk_cer": 1.0,
|
| 212 |
"repetition_rate": 0.0,
|
| 213 |
"table_score": null,
|
| 214 |
+
"elapsed_seconds": 7.9901230840041535,
|
| 215 |
+
"ref_length": 112,
|
| 216 |
+
"hyp_length": 147,
|
| 217 |
+
"ref_digit_count": 30,
|
| 218 |
"ref_cjk_count": 44
|
| 219 |
},
|
| 220 |
{
|
| 221 |
"file": "0011.png",
|
| 222 |
+
"category": "low_quality_scan",
|
| 223 |
"cer": 0.0,
|
| 224 |
"wer": 0.0,
|
| 225 |
"digit_cer": 0.0,
|
| 226 |
"cjk_cer": 0.0,
|
| 227 |
"repetition_rate": 0.0,
|
| 228 |
"table_score": null,
|
| 229 |
+
"elapsed_seconds": 6.431838709002477,
|
| 230 |
+
"ref_length": 120,
|
| 231 |
+
"hyp_length": 120,
|
| 232 |
+
"ref_digit_count": 29,
|
| 233 |
"ref_cjk_count": 0
|
| 234 |
},
|
| 235 |
{
|
| 236 |
"file": "0012.png",
|
| 237 |
+
"category": "low_quality_scan",
|
| 238 |
"cer": 0.0,
|
| 239 |
"wer": 0.0,
|
| 240 |
"digit_cer": 0.0,
|
| 241 |
"cjk_cer": 0.0,
|
| 242 |
"repetition_rate": 0.0,
|
| 243 |
"table_score": null,
|
| 244 |
+
"elapsed_seconds": 6.651832292001927,
|
| 245 |
+
"ref_length": 120,
|
| 246 |
+
"hyp_length": 120,
|
| 247 |
+
"ref_digit_count": 29,
|
| 248 |
"ref_cjk_count": 0
|
| 249 |
}
|
| 250 |
]
|
release/candidate_performance.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
|
|
|
| 3 |
"image_path": "test_invoice.png",
|
| 4 |
"prompt": "<image>document parsing.",
|
| 5 |
"max_tokens": 256,
|
|
@@ -13,55 +14,55 @@
|
|
| 13 |
"chip": "Apple M3 Max",
|
| 14 |
"total_memory_gb": 128.0
|
| 15 |
},
|
| 16 |
-
"model_load_time_seconds": 0.
|
| 17 |
-
"memory_after_load_mb":
|
| 18 |
"num_warmup": 1,
|
| 19 |
"num_runs": 3,
|
| 20 |
-
"mean_tps":
|
| 21 |
-
"std_tps":
|
| 22 |
-
"mean_elapsed_seconds":
|
| 23 |
-
"mean_peak_memory_mb":
|
| 24 |
"runs": [
|
| 25 |
{
|
| 26 |
"run": 1,
|
| 27 |
-
"elapsed_seconds":
|
| 28 |
"tokens_generated": 256,
|
| 29 |
"tokens_generated_source": "mlx-vlm token count",
|
| 30 |
-
"tokens_per_second":
|
| 31 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 32 |
-
"peak_memory_mb":
|
| 33 |
"memory_delta_mb": 1337.1867550000002,
|
| 34 |
-
"process_peak_rss_mb":
|
| 35 |
"prompt_tokens": 697,
|
| 36 |
-
"prompt_tokens_per_second":
|
| 37 |
"finish_reason": "length"
|
| 38 |
},
|
| 39 |
{
|
| 40 |
"run": 2,
|
| 41 |
-
"elapsed_seconds":
|
| 42 |
"tokens_generated": 256,
|
| 43 |
"tokens_generated_source": "mlx-vlm token count",
|
| 44 |
-
"tokens_per_second":
|
| 45 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 46 |
-
"peak_memory_mb":
|
| 47 |
-
"memory_delta_mb": 1337.
|
| 48 |
-
"process_peak_rss_mb":
|
| 49 |
"prompt_tokens": 697,
|
| 50 |
-
"prompt_tokens_per_second":
|
| 51 |
"finish_reason": "length"
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"run": 3,
|
| 55 |
-
"elapsed_seconds":
|
| 56 |
"tokens_generated": 256,
|
| 57 |
"tokens_generated_source": "mlx-vlm token count",
|
| 58 |
-
"tokens_per_second":
|
| 59 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 60 |
-
"peak_memory_mb":
|
| 61 |
-
"memory_delta_mb": 1337.
|
| 62 |
-
"process_peak_rss_mb":
|
| 63 |
"prompt_tokens": 697,
|
| 64 |
-
"prompt_tokens_per_second":
|
| 65 |
"finish_reason": "length"
|
| 66 |
}
|
| 67 |
]
|
|
|
|
| 1 |
{
|
| 2 |
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 3 |
+
"served_revision": null,
|
| 4 |
"image_path": "test_invoice.png",
|
| 5 |
"prompt": "<image>document parsing.",
|
| 6 |
"max_tokens": 256,
|
|
|
|
| 14 |
"chip": "Apple M3 Max",
|
| 15 |
"total_memory_gb": 128.0
|
| 16 |
},
|
| 17 |
+
"model_load_time_seconds": 0.9827843750099419,
|
| 18 |
+
"memory_after_load_mb": 4043.265625,
|
| 19 |
"num_warmup": 1,
|
| 20 |
"num_runs": 3,
|
| 21 |
+
"mean_tps": 35.14004923388335,
|
| 22 |
+
"std_tps": 0.8627581629341593,
|
| 23 |
+
"mean_elapsed_seconds": 8.941998055335716,
|
| 24 |
+
"mean_peak_memory_mb": 5184.062959,
|
| 25 |
"runs": [
|
| 26 |
{
|
| 27 |
"run": 1,
|
| 28 |
+
"elapsed_seconds": 8.715698832995258,
|
| 29 |
"tokens_generated": 256,
|
| 30 |
"tokens_generated_source": "mlx-vlm token count",
|
| 31 |
+
"tokens_per_second": 36.34797807809006,
|
| 32 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 33 |
+
"peak_memory_mb": 5183.887319,
|
| 34 |
"memory_delta_mb": 1337.1867550000002,
|
| 35 |
+
"process_peak_rss_mb": 4058.71875,
|
| 36 |
"prompt_tokens": 697,
|
| 37 |
+
"prompt_tokens_per_second": 422.9274407258055,
|
| 38 |
"finish_reason": "length"
|
| 39 |
},
|
| 40 |
{
|
| 41 |
"run": 2,
|
| 42 |
+
"elapsed_seconds": 9.117345000006026,
|
| 43 |
"tokens_generated": 256,
|
| 44 |
"tokens_generated_source": "mlx-vlm token count",
|
| 45 |
+
"tokens_per_second": 34.685109648852176,
|
| 46 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 47 |
+
"peak_memory_mb": 5184.1507790000005,
|
| 48 |
+
"memory_delta_mb": 1337.1880710000005,
|
| 49 |
+
"process_peak_rss_mb": 4065.359375,
|
| 50 |
"prompt_tokens": 697,
|
| 51 |
+
"prompt_tokens_per_second": 413.1939079712154,
|
| 52 |
"finish_reason": "length"
|
| 53 |
},
|
| 54 |
{
|
| 55 |
"run": 3,
|
| 56 |
+
"elapsed_seconds": 8.992950333005865,
|
| 57 |
"tokens_generated": 256,
|
| 58 |
"tokens_generated_source": "mlx-vlm token count",
|
| 59 |
+
"tokens_per_second": 34.38705997470784,
|
| 60 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 61 |
+
"peak_memory_mb": 5184.1507790000005,
|
| 62 |
+
"memory_delta_mb": 1337.1880710000005,
|
| 63 |
+
"process_peak_rss_mb": 4065.5625,
|
| 64 |
"prompt_tokens": 697,
|
| 65 |
+
"prompt_tokens_per_second": 463.0680271067021,
|
| 66 |
"finish_reason": "length"
|
| 67 |
}
|
| 68 |
]
|
release/candidate_rswa.json
CHANGED
|
@@ -3,23 +3,28 @@
|
|
| 3 |
"image_path": "test_invoice.png",
|
| 4 |
"prompt": "<image>document parsing.",
|
| 5 |
"force_min_tokens": 8192,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"forced_eos_token_ids": [
|
| 7 |
1
|
| 8 |
],
|
| 9 |
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 10 |
"processor": "arm",
|
| 11 |
-
"baseline_memory_mb":
|
| 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":
|
| 19 |
-
"mean_tps":
|
| 20 |
"tps_source": "mlx-vlm generation_tps",
|
| 21 |
-
"peak_memory_mb":
|
| 22 |
-
"memory_growth_mb": 1337.
|
| 23 |
"repetition_rate": 0.0,
|
| 24 |
"completed_naturally": false,
|
| 25 |
"finish_reason": "length",
|
|
@@ -31,12 +36,12 @@
|
|
| 31 |
"tokens_generated": 2048,
|
| 32 |
"tokens_generated_source": "mlx-vlm token count",
|
| 33 |
"tokens_generated_reliable": true,
|
| 34 |
-
"elapsed_seconds":
|
| 35 |
-
"mean_tps":
|
| 36 |
"tps_source": "mlx-vlm generation_tps",
|
| 37 |
-
"peak_memory_mb":
|
| 38 |
-
"memory_growth_mb": 1337.
|
| 39 |
-
"repetition_rate": 0.
|
| 40 |
"completed_naturally": false,
|
| 41 |
"finish_reason": "length",
|
| 42 |
"forced_minimum_tokens": 2048,
|
|
@@ -47,12 +52,12 @@
|
|
| 47 |
"tokens_generated": 8192,
|
| 48 |
"tokens_generated_source": "mlx-vlm token count",
|
| 49 |
"tokens_generated_reliable": true,
|
| 50 |
-
"elapsed_seconds":
|
| 51 |
-
"mean_tps":
|
| 52 |
"tps_source": "mlx-vlm generation_tps",
|
| 53 |
-
"peak_memory_mb":
|
| 54 |
-
"memory_growth_mb": 1337.
|
| 55 |
-
"repetition_rate": 0.
|
| 56 |
"completed_naturally": false,
|
| 57 |
"finish_reason": "length",
|
| 58 |
"forced_minimum_tokens": 8192,
|
|
@@ -64,7 +69,7 @@
|
|
| 64 |
"cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
|
| 65 |
"tps_stable_over_length": true,
|
| 66 |
"max_successful_tokens": 8192,
|
| 67 |
-
"max_repetition_rate": 0.
|
| 68 |
},
|
| 69 |
"pass_conditions": {
|
| 70 |
"cache_bounded": true,
|
|
|
|
| 3 |
"image_path": "test_invoice.png",
|
| 4 |
"prompt": "<image>document parsing.",
|
| 5 |
"force_min_tokens": 8192,
|
| 6 |
+
"generation_settings": {
|
| 7 |
+
"temperature": 0.0,
|
| 8 |
+
"no_repeat_ngram_size": 35,
|
| 9 |
+
"ngram_window": 128
|
| 10 |
+
},
|
| 11 |
"forced_eos_token_ids": [
|
| 12 |
1
|
| 13 |
],
|
| 14 |
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 15 |
"processor": "arm",
|
| 16 |
+
"baseline_memory_mb": 3846.700552,
|
| 17 |
"test_results": [
|
| 18 |
{
|
| 19 |
"max_tokens": 512,
|
| 20 |
"tokens_generated": 512,
|
| 21 |
"tokens_generated_source": "mlx-vlm token count",
|
| 22 |
"tokens_generated_reliable": true,
|
| 23 |
+
"elapsed_seconds": 17.311166541010607,
|
| 24 |
+
"mean_tps": 32.76836342370835,
|
| 25 |
"tps_source": "mlx-vlm generation_tps",
|
| 26 |
+
"peak_memory_mb": 5183.890113,
|
| 27 |
+
"memory_growth_mb": 1337.1895610000006,
|
| 28 |
"repetition_rate": 0.0,
|
| 29 |
"completed_naturally": false,
|
| 30 |
"finish_reason": "length",
|
|
|
|
| 36 |
"tokens_generated": 2048,
|
| 37 |
"tokens_generated_source": "mlx-vlm token count",
|
| 38 |
"tokens_generated_reliable": true,
|
| 39 |
+
"elapsed_seconds": 65.73962916599703,
|
| 40 |
+
"mean_tps": 32.0896585893278,
|
| 41 |
"tps_source": "mlx-vlm generation_tps",
|
| 42 |
+
"peak_memory_mb": 5184.152257,
|
| 43 |
+
"memory_growth_mb": 1337.1895489999997,
|
| 44 |
+
"repetition_rate": 0.20496894409937888,
|
| 45 |
"completed_naturally": false,
|
| 46 |
"finish_reason": "length",
|
| 47 |
"forced_minimum_tokens": 2048,
|
|
|
|
| 52 |
"tokens_generated": 8192,
|
| 53 |
"tokens_generated_source": "mlx-vlm token count",
|
| 54 |
"tokens_generated_reliable": true,
|
| 55 |
+
"elapsed_seconds": 268.4810879580036,
|
| 56 |
+
"mean_tps": 30.743604031277776,
|
| 57 |
"tps_source": "mlx-vlm generation_tps",
|
| 58 |
+
"peak_memory_mb": 5184.152257,
|
| 59 |
+
"memory_growth_mb": 1337.1895489999997,
|
| 60 |
+
"repetition_rate": 0.20496894409937888,
|
| 61 |
"completed_naturally": false,
|
| 62 |
"finish_reason": "length",
|
| 63 |
"forced_minimum_tokens": 8192,
|
|
|
|
| 69 |
"cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
|
| 70 |
"tps_stable_over_length": true,
|
| 71 |
"max_successful_tokens": 8192,
|
| 72 |
+
"max_repetition_rate": 0.20496894409937888
|
| 73 |
},
|
| 74 |
"pass_conditions": {
|
| 75 |
"cache_bounded": true,
|
release/generated_precision_map.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"vision_model": "bfloat16",
|
| 3 |
-
"projector": "
|
| 4 |
"language_model.model.embed_tokens": "mxfp8",
|
| 5 |
"language_model.model.norm": "bfloat16",
|
| 6 |
"language_model.lm_head": "affine8",
|
|
@@ -32,16 +32,16 @@
|
|
| 32 |
"vision_encoder": {
|
| 33 |
"precision": "bfloat16",
|
| 34 |
"reasons": [
|
| 35 |
-
"cer_delta=2.
|
| 36 |
-
"digit_cer_delta=1.
|
| 37 |
"table_score_degradation=1.000000",
|
| 38 |
"protected OCR-critical group"
|
| 39 |
]
|
| 40 |
},
|
| 41 |
"vision_projector": {
|
| 42 |
-
"precision": "
|
| 43 |
"reasons": [
|
| 44 |
-
"all measured quality deltas
|
| 45 |
]
|
| 46 |
},
|
| 47 |
"token_embeddings": {
|
|
|
|
| 1 |
{
|
| 2 |
"vision_model": "bfloat16",
|
| 3 |
+
"projector": "bfloat16",
|
| 4 |
"language_model.model.embed_tokens": "mxfp8",
|
| 5 |
"language_model.model.norm": "bfloat16",
|
| 6 |
"language_model.lm_head": "affine8",
|
|
|
|
| 32 |
"vision_encoder": {
|
| 33 |
"precision": "bfloat16",
|
| 34 |
"reasons": [
|
| 35 |
+
"cer_delta=2.272233",
|
| 36 |
+
"digit_cer_delta=1.696454",
|
| 37 |
"table_score_degradation=1.000000",
|
| 38 |
"protected OCR-critical group"
|
| 39 |
]
|
| 40 |
},
|
| 41 |
"vision_projector": {
|
| 42 |
+
"precision": "bfloat16",
|
| 43 |
"reasons": [
|
| 44 |
+
"protected unless all measured quality deltas are non-degrading"
|
| 45 |
]
|
| 46 |
},
|
| 47 |
"token_embeddings": {
|
release/provenance.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"created_at": "2026-07-
|
| 3 |
"python": "3.14.6",
|
| 4 |
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 5 |
"machine": "arm64",
|
|
@@ -12,27 +12,29 @@
|
|
| 12 |
"Pillow": "12.3.0"
|
| 13 |
},
|
| 14 |
"source_model": "baidu/Unlimited-OCR",
|
|
|
|
| 15 |
"source_local_name": "Unlimited-OCR",
|
| 16 |
"source_config": {
|
| 17 |
"path": "config.json",
|
| 18 |
"sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9"
|
| 19 |
},
|
| 20 |
"reference_model": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
|
|
|
| 21 |
"target_repo": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 22 |
-
"
|
| 23 |
-
"name": "
|
| 24 |
-
"num_samples":
|
| 25 |
-
"num_digit_samples":
|
| 26 |
"num_cjk_samples": 3,
|
| 27 |
"num_table_samples": 3,
|
| 28 |
"category_counts": {
|
| 29 |
"cjk_document": 3,
|
| 30 |
-
"english_document":
|
| 31 |
"low_quality_scan": 2,
|
| 32 |
"table_numeric": 3
|
| 33 |
},
|
| 34 |
-
"manifest_sha256": "
|
| 35 |
-
"content_sha256": "
|
| 36 |
"files": [
|
| 37 |
{
|
| 38 |
"path": "images/0001.png",
|
|
@@ -56,103 +58,220 @@
|
|
| 56 |
},
|
| 57 |
{
|
| 58 |
"path": "images/0003.png",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
"size": 181040,
|
| 60 |
"sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
|
| 61 |
},
|
| 62 |
{
|
| 63 |
-
"path": "ground_truth/
|
| 64 |
"size": 1333,
|
| 65 |
"sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
|
| 66 |
},
|
| 67 |
{
|
| 68 |
-
"path": "images/
|
| 69 |
"size": 220208,
|
| 70 |
"sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
|
| 71 |
},
|
| 72 |
{
|
| 73 |
-
"path": "ground_truth/
|
| 74 |
"size": 1330,
|
| 75 |
"sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
|
| 76 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
{
|
| 78 |
"path": "images/0005.png",
|
| 79 |
-
"size":
|
| 80 |
-
"sha256": "
|
| 81 |
},
|
| 82 |
{
|
| 83 |
"path": "ground_truth/0005.txt",
|
| 84 |
-
"size":
|
| 85 |
-
"sha256": "
|
| 86 |
},
|
| 87 |
{
|
| 88 |
"path": "images/0006.png",
|
| 89 |
-
"size":
|
| 90 |
-
"sha256": "
|
| 91 |
},
|
| 92 |
{
|
| 93 |
"path": "ground_truth/0006.txt",
|
| 94 |
-
"size":
|
| 95 |
-
"sha256": "
|
| 96 |
},
|
| 97 |
{
|
| 98 |
"path": "images/0007.png",
|
| 99 |
-
"size":
|
| 100 |
-
"sha256": "
|
| 101 |
},
|
| 102 |
{
|
| 103 |
"path": "ground_truth/0007.txt",
|
| 104 |
-
"size":
|
| 105 |
-
"sha256": "
|
| 106 |
},
|
| 107 |
{
|
| 108 |
"path": "images/0008.png",
|
| 109 |
-
"size":
|
| 110 |
-
"sha256": "
|
| 111 |
},
|
| 112 |
{
|
| 113 |
"path": "ground_truth/0008.txt",
|
| 114 |
-
"size":
|
| 115 |
-
"sha256": "
|
| 116 |
},
|
| 117 |
{
|
| 118 |
"path": "images/0009.png",
|
| 119 |
-
"size":
|
| 120 |
-
"sha256": "
|
| 121 |
},
|
| 122 |
{
|
| 123 |
"path": "ground_truth/0009.txt",
|
| 124 |
-
"size":
|
| 125 |
-
"sha256": "
|
| 126 |
},
|
| 127 |
{
|
| 128 |
"path": "images/0010.png",
|
| 129 |
-
"size":
|
| 130 |
-
"sha256": "
|
| 131 |
},
|
| 132 |
{
|
| 133 |
"path": "ground_truth/0010.txt",
|
| 134 |
-
"size":
|
| 135 |
-
"sha256": "
|
| 136 |
},
|
| 137 |
{
|
| 138 |
"path": "images/0011.png",
|
| 139 |
-
"size":
|
| 140 |
-
"sha256": "
|
| 141 |
},
|
| 142 |
{
|
| 143 |
"path": "ground_truth/0011.txt",
|
| 144 |
-
"size":
|
| 145 |
-
"sha256": "
|
| 146 |
},
|
| 147 |
{
|
| 148 |
"path": "images/0012.png",
|
| 149 |
-
"size":
|
| 150 |
-
"sha256": "
|
| 151 |
},
|
| 152 |
{
|
| 153 |
"path": "ground_truth/0012.txt",
|
| 154 |
-
"size":
|
| 155 |
-
"sha256": "
|
| 156 |
}
|
| 157 |
]
|
| 158 |
},
|
|
|
|
| 1 |
{
|
| 2 |
+
"created_at": "2026-07-21T03:08:54.605846+00:00",
|
| 3 |
"python": "3.14.6",
|
| 4 |
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 5 |
"machine": "arm64",
|
|
|
|
| 12 |
"Pillow": "12.3.0"
|
| 13 |
},
|
| 14 |
"source_model": "baidu/Unlimited-OCR",
|
| 15 |
+
"source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 16 |
"source_local_name": "Unlimited-OCR",
|
| 17 |
"source_config": {
|
| 18 |
"path": "config.json",
|
| 19 |
"sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9"
|
| 20 |
},
|
| 21 |
"reference_model": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 22 |
+
"reference_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 23 |
"target_repo": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 24 |
+
"calibration_dataset": {
|
| 25 |
+
"name": "calibration",
|
| 26 |
+
"num_samples": 10,
|
| 27 |
+
"num_digit_samples": 10,
|
| 28 |
"num_cjk_samples": 3,
|
| 29 |
"num_table_samples": 3,
|
| 30 |
"category_counts": {
|
| 31 |
"cjk_document": 3,
|
| 32 |
+
"english_document": 2,
|
| 33 |
"low_quality_scan": 2,
|
| 34 |
"table_numeric": 3
|
| 35 |
},
|
| 36 |
+
"manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
|
| 37 |
+
"content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 38 |
"files": [
|
| 39 |
{
|
| 40 |
"path": "images/0001.png",
|
|
|
|
| 58 |
},
|
| 59 |
{
|
| 60 |
"path": "images/0003.png",
|
| 61 |
+
"size": 40039,
|
| 62 |
+
"sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"path": "ground_truth/0003.txt",
|
| 66 |
+
"size": 459,
|
| 67 |
+
"sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"path": "images/0004.png",
|
| 71 |
+
"size": 40356,
|
| 72 |
+
"sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"path": "ground_truth/0004.txt",
|
| 76 |
+
"size": 459,
|
| 77 |
+
"sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"path": "images/0005.png",
|
| 81 |
+
"size": 40650,
|
| 82 |
+
"sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"path": "ground_truth/0005.txt",
|
| 86 |
+
"size": 459,
|
| 87 |
+
"sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"path": "images/0006.png",
|
| 91 |
+
"size": 46036,
|
| 92 |
+
"sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"path": "ground_truth/0006.txt",
|
| 96 |
+
"size": 191,
|
| 97 |
+
"sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"path": "images/0007.png",
|
| 101 |
+
"size": 41828,
|
| 102 |
+
"sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"path": "ground_truth/0007.txt",
|
| 106 |
+
"size": 195,
|
| 107 |
+
"sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"path": "images/0008.png",
|
| 111 |
+
"size": 20975,
|
| 112 |
+
"sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"path": "ground_truth/0008.txt",
|
| 116 |
+
"size": 201,
|
| 117 |
+
"sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"path": "images/0009.png",
|
| 121 |
+
"size": 487313,
|
| 122 |
+
"sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"path": "ground_truth/0009.txt",
|
| 126 |
+
"size": 121,
|
| 127 |
+
"sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"path": "images/0010.png",
|
| 131 |
+
"size": 515820,
|
| 132 |
+
"sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"path": "ground_truth/0010.txt",
|
| 136 |
+
"size": 121,
|
| 137 |
+
"sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
|
| 138 |
+
}
|
| 139 |
+
]
|
| 140 |
+
},
|
| 141 |
+
"evaluation_dataset": {
|
| 142 |
+
"name": "evaluation",
|
| 143 |
+
"num_samples": 12,
|
| 144 |
+
"num_digit_samples": 12,
|
| 145 |
+
"num_cjk_samples": 3,
|
| 146 |
+
"num_table_samples": 3,
|
| 147 |
+
"category_counts": {
|
| 148 |
+
"cjk_document": 3,
|
| 149 |
+
"english_document": 4,
|
| 150 |
+
"low_quality_scan": 2,
|
| 151 |
+
"table_numeric": 3
|
| 152 |
+
},
|
| 153 |
+
"manifest_sha256": "111646a6f59ed32689fbc658c1cb4ed60629e1bbd909d6137df469e7b489f121",
|
| 154 |
+
"content_sha256": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
|
| 155 |
+
"files": [
|
| 156 |
+
{
|
| 157 |
+
"path": "images/0001.png",
|
| 158 |
"size": 181040,
|
| 159 |
"sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
|
| 160 |
},
|
| 161 |
{
|
| 162 |
+
"path": "ground_truth/0001.txt",
|
| 163 |
"size": 1333,
|
| 164 |
"sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
|
| 165 |
},
|
| 166 |
{
|
| 167 |
+
"path": "images/0002.png",
|
| 168 |
"size": 220208,
|
| 169 |
"sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
|
| 170 |
},
|
| 171 |
{
|
| 172 |
+
"path": "ground_truth/0002.txt",
|
| 173 |
"size": 1330,
|
| 174 |
"sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
|
| 175 |
},
|
| 176 |
+
{
|
| 177 |
+
"path": "images/0003.png",
|
| 178 |
+
"size": 119613,
|
| 179 |
+
"sha256": "bcf22c94459b970f1ee11088d8a902dbc56975e487c489b4714dedf8f3a4dca1"
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"path": "ground_truth/0003.txt",
|
| 183 |
+
"size": 637,
|
| 184 |
+
"sha256": "a035ed288c8cc0e73e6cddc08c804be9b1ed32eee98e7f85c0e027bd88221b66"
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"path": "images/0004.png",
|
| 188 |
+
"size": 67701,
|
| 189 |
+
"sha256": "baea8365beb81cc9939e59d3c8a0b88ab17999122eb1729e33214450a4fba987"
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"path": "ground_truth/0004.txt",
|
| 193 |
+
"size": 478,
|
| 194 |
+
"sha256": "af1c29967e55595362ab41e25000e00167474cbe38ea43b8816f1f1395ef6e72"
|
| 195 |
+
},
|
| 196 |
{
|
| 197 |
"path": "images/0005.png",
|
| 198 |
+
"size": 41300,
|
| 199 |
+
"sha256": "65662634fc3b5df7d006f40d276fb2869809a86a2027f464c1d6776094e16f16"
|
| 200 |
},
|
| 201 |
{
|
| 202 |
"path": "ground_truth/0005.txt",
|
| 203 |
+
"size": 459,
|
| 204 |
+
"sha256": "1d565459641aca5056fdcccabe2cdc6834107bef219645a54cd28db2e06b05ea"
|
| 205 |
},
|
| 206 |
{
|
| 207 |
"path": "images/0006.png",
|
| 208 |
+
"size": 40574,
|
| 209 |
+
"sha256": "67dbd4d07cb6e3f971937dc93a35ac81775580ee4d8895373afe6ae7e6b328dd"
|
| 210 |
},
|
| 211 |
{
|
| 212 |
"path": "ground_truth/0006.txt",
|
| 213 |
+
"size": 459,
|
| 214 |
+
"sha256": "0e134815b0bee585e3841d5a22a296c136fb43f00a9e3d48d98d2df24e884b08"
|
| 215 |
},
|
| 216 |
{
|
| 217 |
"path": "images/0007.png",
|
| 218 |
+
"size": 41027,
|
| 219 |
+
"sha256": "2b54b5222174bf4d41d92a6ee97aa1b2a3e57daca987b55906793df807dfe08f"
|
| 220 |
},
|
| 221 |
{
|
| 222 |
"path": "ground_truth/0007.txt",
|
| 223 |
+
"size": 459,
|
| 224 |
+
"sha256": "b63a7c471f4d36cc8bafddcbd7b4a8d92d6f92e73ab1e260505c2413d6e39146"
|
| 225 |
},
|
| 226 |
{
|
| 227 |
"path": "images/0008.png",
|
| 228 |
+
"size": 46025,
|
| 229 |
+
"sha256": "c8c8ed310cbdebf7e27adf3361e6ce5939922bd674af12205cea441a9b53399d"
|
| 230 |
},
|
| 231 |
{
|
| 232 |
"path": "ground_truth/0008.txt",
|
| 233 |
+
"size": 191,
|
| 234 |
+
"sha256": "246e79c3ef9faa52dd10c400eecf594314493c88ae5f7794a6129dd90e850107"
|
| 235 |
},
|
| 236 |
{
|
| 237 |
"path": "images/0009.png",
|
| 238 |
+
"size": 41617,
|
| 239 |
+
"sha256": "a00eaebf40f9369b6851a949a0aedc82a57cf1104a957e42880b1f3aa65606f0"
|
| 240 |
},
|
| 241 |
{
|
| 242 |
"path": "ground_truth/0009.txt",
|
| 243 |
+
"size": 195,
|
| 244 |
+
"sha256": "5653ef6df8a330496e73c6ddd71c9b2280818e13b11b6ff8842622ad53d24a5b"
|
| 245 |
},
|
| 246 |
{
|
| 247 |
"path": "images/0010.png",
|
| 248 |
+
"size": 21089,
|
| 249 |
+
"sha256": "59b6edec6b2ef0369f1d7fbe9fe96648fda2982db0eae377199d5edf26855900"
|
| 250 |
},
|
| 251 |
{
|
| 252 |
"path": "ground_truth/0010.txt",
|
| 253 |
+
"size": 201,
|
| 254 |
+
"sha256": "6d2f6474c94dea173288aebde046b6db40534548e8187cb776dc494abaa04ce2"
|
| 255 |
},
|
| 256 |
{
|
| 257 |
"path": "images/0011.png",
|
| 258 |
+
"size": 487332,
|
| 259 |
+
"sha256": "70f57ebf6f41479e443e4356422e6dd619ffdcf2f0df7837943f5427545cd266"
|
| 260 |
},
|
| 261 |
{
|
| 262 |
"path": "ground_truth/0011.txt",
|
| 263 |
+
"size": 121,
|
| 264 |
+
"sha256": "f550334b0b7d3508ef898d0e144593c6d005dd32d1bbcb837aeb650da56e9eb8"
|
| 265 |
},
|
| 266 |
{
|
| 267 |
"path": "images/0012.png",
|
| 268 |
+
"size": 516088,
|
| 269 |
+
"sha256": "fdd426f69928ba7bcc6008655fa0dbd950b6844eba8322491e7a5399bf052d59"
|
| 270 |
},
|
| 271 |
{
|
| 272 |
"path": "ground_truth/0012.txt",
|
| 273 |
+
"size": 121,
|
| 274 |
+
"sha256": "d040adc39553bc337df562486b3b5148727660214ec79a3af48b9cf5a2bc23f7"
|
| 275 |
}
|
| 276 |
]
|
| 277 |
},
|
release/reference_accuracy.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
|
|
|
|
|
|
| 3 |
"prompt": "<image>document parsing.",
|
| 4 |
"max_tokens": 1024,
|
| 5 |
"profile": "accurate",
|
|
@@ -12,183 +14,237 @@
|
|
| 12 |
},
|
| 13 |
"num_images": 12,
|
| 14 |
"num_samples": 12,
|
| 15 |
-
"total_time_seconds":
|
| 16 |
-
"mean_cer":
|
| 17 |
-
"mean_wer":
|
| 18 |
"num_digit_samples": 12,
|
| 19 |
"num_cjk_samples": 3,
|
| 20 |
-
"mean_digit_cer": 0.
|
| 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 |
-
"
|
| 29 |
-
"
|
| 30 |
-
"
|
|
|
|
| 31 |
"cjk_cer": 0.0,
|
| 32 |
-
"repetition_rate": 0.
|
| 33 |
"table_score": null,
|
| 34 |
-
"elapsed_seconds":
|
| 35 |
-
"ref_length":
|
| 36 |
-
"hyp_length":
|
| 37 |
-
"ref_digit_count":
|
| 38 |
"ref_cjk_count": 0
|
| 39 |
},
|
| 40 |
{
|
| 41 |
"file": "0002.png",
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
|
|
|
| 45 |
"cjk_cer": 0.0,
|
| 46 |
"repetition_rate": 0.0,
|
| 47 |
"table_score": null,
|
| 48 |
-
"elapsed_seconds":
|
| 49 |
-
"ref_length":
|
| 50 |
-
"hyp_length":
|
| 51 |
-
"ref_digit_count":
|
| 52 |
"ref_cjk_count": 0
|
| 53 |
},
|
| 54 |
{
|
| 55 |
"file": "0003.png",
|
| 56 |
-
"
|
| 57 |
-
"
|
| 58 |
-
"
|
|
|
|
| 59 |
"cjk_cer": 0.0,
|
| 60 |
-
"repetition_rate": 0.
|
| 61 |
"table_score": null,
|
| 62 |
-
"elapsed_seconds":
|
| 63 |
-
"ref_length":
|
| 64 |
-
"hyp_length":
|
| 65 |
-
"ref_digit_count":
|
| 66 |
"ref_cjk_count": 0
|
| 67 |
},
|
| 68 |
{
|
| 69 |
"file": "0004.png",
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"
|
|
|
|
| 73 |
"cjk_cer": 0.0,
|
| 74 |
"repetition_rate": 0.0,
|
| 75 |
"table_score": null,
|
| 76 |
-
"elapsed_seconds":
|
| 77 |
-
"ref_length":
|
| 78 |
-
"hyp_length":
|
| 79 |
-
"ref_digit_count":
|
| 80 |
"ref_cjk_count": 0
|
| 81 |
},
|
| 82 |
{
|
| 83 |
"file": "0005.png",
|
| 84 |
-
"
|
|
|
|
| 85 |
"wer": 1.5,
|
| 86 |
-
"digit_cer": 0.
|
| 87 |
"cjk_cer": 0.0,
|
| 88 |
"repetition_rate": 0.0,
|
| 89 |
"table_score": 1.0,
|
| 90 |
-
"elapsed_seconds":
|
| 91 |
-
"ref_length":
|
| 92 |
-
"hyp_length":
|
| 93 |
-
"ref_digit_count":
|
| 94 |
"ref_cjk_count": 0
|
| 95 |
},
|
| 96 |
{
|
| 97 |
"file": "0006.png",
|
| 98 |
-
"
|
|
|
|
| 99 |
"wer": 1.5,
|
| 100 |
-
"digit_cer": 0.
|
| 101 |
"cjk_cer": 0.0,
|
| 102 |
"repetition_rate": 0.0,
|
| 103 |
"table_score": 1.0,
|
| 104 |
-
"elapsed_seconds":
|
| 105 |
-
"ref_length":
|
| 106 |
-
"hyp_length":
|
| 107 |
-
"ref_digit_count":
|
| 108 |
"ref_cjk_count": 0
|
| 109 |
},
|
| 110 |
{
|
| 111 |
"file": "0007.png",
|
| 112 |
-
"
|
|
|
|
| 113 |
"wer": 1.5,
|
| 114 |
-
"digit_cer": 0.
|
| 115 |
"cjk_cer": 0.0,
|
| 116 |
"repetition_rate": 0.0,
|
| 117 |
"table_score": 1.0,
|
| 118 |
-
"elapsed_seconds":
|
| 119 |
-
"ref_length":
|
| 120 |
-
"hyp_length":
|
| 121 |
-
"ref_digit_count":
|
| 122 |
"ref_cjk_count": 0
|
| 123 |
},
|
| 124 |
{
|
| 125 |
"file": "0008.png",
|
| 126 |
-
"
|
|
|
|
| 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":
|
| 133 |
-
"ref_length":
|
| 134 |
-
"hyp_length":
|
| 135 |
-
"ref_digit_count":
|
| 136 |
"ref_cjk_count": 41
|
| 137 |
},
|
| 138 |
{
|
| 139 |
"file": "0009.png",
|
| 140 |
-
"
|
|
|
|
| 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":
|
| 147 |
-
"ref_length":
|
| 148 |
-
"hyp_length":
|
| 149 |
-
"ref_digit_count":
|
| 150 |
"ref_cjk_count": 42
|
| 151 |
},
|
| 152 |
{
|
| 153 |
"file": "0010.png",
|
| 154 |
-
"
|
|
|
|
| 155 |
"wer": 1.9545454545454546,
|
| 156 |
-
"digit_cer": 0.
|
| 157 |
"cjk_cer": 1.0,
|
| 158 |
"repetition_rate": 0.0,
|
| 159 |
"table_score": null,
|
| 160 |
-
"elapsed_seconds":
|
| 161 |
-
"ref_length":
|
| 162 |
-
"hyp_length":
|
| 163 |
-
"ref_digit_count":
|
| 164 |
"ref_cjk_count": 44
|
| 165 |
},
|
| 166 |
{
|
| 167 |
"file": "0011.png",
|
| 168 |
-
"
|
| 169 |
-
"
|
| 170 |
-
"
|
|
|
|
| 171 |
"cjk_cer": 0.0,
|
| 172 |
"repetition_rate": 0.0,
|
| 173 |
"table_score": null,
|
| 174 |
-
"elapsed_seconds":
|
| 175 |
-
"ref_length":
|
| 176 |
-
"hyp_length":
|
| 177 |
-
"ref_digit_count":
|
| 178 |
"ref_cjk_count": 0
|
| 179 |
},
|
| 180 |
{
|
| 181 |
"file": "0012.png",
|
| 182 |
-
"
|
| 183 |
-
"
|
|
|
|
| 184 |
"digit_cer": 0.0,
|
| 185 |
"cjk_cer": 0.0,
|
| 186 |
"repetition_rate": 0.0,
|
| 187 |
"table_score": null,
|
| 188 |
-
"elapsed_seconds":
|
| 189 |
-
"ref_length":
|
| 190 |
-
"hyp_length":
|
| 191 |
-
"ref_digit_count":
|
| 192 |
"ref_cjk_count": 0
|
| 193 |
}
|
| 194 |
]
|
|
|
|
| 1 |
{
|
| 2 |
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 3 |
+
"served_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 4 |
+
"backend": "mlx",
|
| 5 |
"prompt": "<image>document parsing.",
|
| 6 |
"max_tokens": 1024,
|
| 7 |
"profile": "accurate",
|
|
|
|
| 14 |
},
|
| 15 |
"num_images": 12,
|
| 16 |
"num_samples": 12,
|
| 17 |
+
"total_time_seconds": 162.6677352060069,
|
| 18 |
+
"mean_cer": 0.9073812810686629,
|
| 19 |
+
"mean_wer": 1.001372473311303,
|
| 20 |
"num_digit_samples": 12,
|
| 21 |
"num_cjk_samples": 3,
|
| 22 |
+
"mean_digit_cer": 0.4065110611974061,
|
| 23 |
"mean_cjk_cer": 1.0,
|
| 24 |
"num_table_samples": 3,
|
| 25 |
"mean_table_score": 1.0,
|
| 26 |
"mean_repetition_rate": 0.00026371308016877635,
|
| 27 |
+
"by_category": {
|
| 28 |
+
"cjk_document": {
|
| 29 |
+
"num_samples": 3,
|
| 30 |
+
"mean_cer": 2.317035024900193,
|
| 31 |
+
"mean_digit_cer": 0.08888888888888889,
|
| 32 |
+
"mean_cjk_cer": 1.0,
|
| 33 |
+
"mean_table_score": null,
|
| 34 |
+
"num_digit_samples": 3,
|
| 35 |
+
"num_cjk_samples": 3,
|
| 36 |
+
"num_table_samples": 0
|
| 37 |
+
},
|
| 38 |
+
"english_document": {
|
| 39 |
+
"num_samples": 4,
|
| 40 |
+
"mean_cer": 0.8171043138735056,
|
| 41 |
+
"mean_digit_cer": 0.9432759996841724,
|
| 42 |
+
"mean_cjk_cer": null,
|
| 43 |
+
"mean_table_score": null,
|
| 44 |
+
"num_digit_samples": 4,
|
| 45 |
+
"num_cjk_samples": 0,
|
| 46 |
+
"num_table_samples": 0
|
| 47 |
+
},
|
| 48 |
+
"low_quality_scan": {
|
| 49 |
+
"num_samples": 2,
|
| 50 |
+
"mean_cer": 0.07142857142857142,
|
| 51 |
+
"mean_digit_cer": 0.13793103448275862,
|
| 52 |
+
"mean_cjk_cer": null,
|
| 53 |
+
"mean_table_score": null,
|
| 54 |
+
"num_digit_samples": 2,
|
| 55 |
+
"num_cjk_samples": 0,
|
| 56 |
+
"num_table_samples": 0
|
| 57 |
+
},
|
| 58 |
+
"table_numeric": {
|
| 59 |
+
"num_samples": 3,
|
| 60 |
+
"mean_cer": 0.1753986332574032,
|
| 61 |
+
"mean_digit_cer": 0.1875,
|
| 62 |
+
"mean_cjk_cer": null,
|
| 63 |
+
"mean_table_score": 1.0,
|
| 64 |
+
"num_digit_samples": 3,
|
| 65 |
+
"num_cjk_samples": 0,
|
| 66 |
+
"num_table_samples": 3
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
"per_file": [
|
| 70 |
{
|
| 71 |
"file": "0001.png",
|
| 72 |
+
"category": "english_document",
|
| 73 |
+
"cer": 1.1751563896336015,
|
| 74 |
+
"wer": 1.439252336448598,
|
| 75 |
+
"digit_cer": 0.48148148148148145,
|
| 76 |
"cjk_cer": 0.0,
|
| 77 |
+
"repetition_rate": 0.0031645569620253164,
|
| 78 |
"table_score": null,
|
| 79 |
+
"elapsed_seconds": 32.467281207995256,
|
| 80 |
+
"ref_length": 1332,
|
| 81 |
+
"hyp_length": 2457,
|
| 82 |
+
"ref_digit_count": 27,
|
| 83 |
"ref_cjk_count": 0
|
| 84 |
},
|
| 85 |
{
|
| 86 |
"file": "0002.png",
|
| 87 |
+
"category": "english_document",
|
| 88 |
+
"cer": 0.7319494584837545,
|
| 89 |
+
"wer": 0.6216216216216216,
|
| 90 |
+
"digit_cer": 0.288135593220339,
|
| 91 |
"cjk_cer": 0.0,
|
| 92 |
"repetition_rate": 0.0,
|
| 93 |
"table_score": null,
|
| 94 |
+
"elapsed_seconds": 31.99284904099477,
|
| 95 |
+
"ref_length": 1329,
|
| 96 |
+
"hyp_length": 1794,
|
| 97 |
+
"ref_digit_count": 59,
|
| 98 |
"ref_cjk_count": 0
|
| 99 |
},
|
| 100 |
{
|
| 101 |
"file": "0003.png",
|
| 102 |
+
"category": "english_document",
|
| 103 |
+
"cer": 1.2802303262955854,
|
| 104 |
+
"wer": 1.2586206896551724,
|
| 105 |
+
"digit_cer": 2.767123287671233,
|
| 106 |
"cjk_cer": 0.0,
|
| 107 |
+
"repetition_rate": 0.0,
|
| 108 |
"table_score": null,
|
| 109 |
+
"elapsed_seconds": 30.016402541004936,
|
| 110 |
+
"ref_length": 636,
|
| 111 |
+
"hyp_length": 1349,
|
| 112 |
+
"ref_digit_count": 73,
|
| 113 |
"ref_cjk_count": 0
|
| 114 |
},
|
| 115 |
{
|
| 116 |
"file": "0004.png",
|
| 117 |
+
"category": "english_document",
|
| 118 |
+
"cer": 0.08108108108108109,
|
| 119 |
+
"wer": 0.15492957746478872,
|
| 120 |
+
"digit_cer": 0.23636363636363636,
|
| 121 |
"cjk_cer": 0.0,
|
| 122 |
"repetition_rate": 0.0,
|
| 123 |
"table_score": null,
|
| 124 |
+
"elapsed_seconds": 12.61737366700254,
|
| 125 |
+
"ref_length": 477,
|
| 126 |
+
"hyp_length": 478,
|
| 127 |
+
"ref_digit_count": 55,
|
| 128 |
"ref_cjk_count": 0
|
| 129 |
},
|
| 130 |
{
|
| 131 |
"file": "0005.png",
|
| 132 |
+
"category": "table_numeric",
|
| 133 |
+
"cer": 0.17539863325740318,
|
| 134 |
"wer": 1.5,
|
| 135 |
+
"digit_cer": 0.1875,
|
| 136 |
"cjk_cer": 0.0,
|
| 137 |
"repetition_rate": 0.0,
|
| 138 |
"table_score": 1.0,
|
| 139 |
+
"elapsed_seconds": 8.039810500005842,
|
| 140 |
+
"ref_length": 446,
|
| 141 |
+
"hyp_length": 519,
|
| 142 |
+
"ref_digit_count": 80,
|
| 143 |
"ref_cjk_count": 0
|
| 144 |
},
|
| 145 |
{
|
| 146 |
"file": "0006.png",
|
| 147 |
+
"category": "table_numeric",
|
| 148 |
+
"cer": 0.17539863325740318,
|
| 149 |
"wer": 1.5,
|
| 150 |
+
"digit_cer": 0.1875,
|
| 151 |
"cjk_cer": 0.0,
|
| 152 |
"repetition_rate": 0.0,
|
| 153 |
"table_score": 1.0,
|
| 154 |
+
"elapsed_seconds": 8.472794083005283,
|
| 155 |
+
"ref_length": 446,
|
| 156 |
+
"hyp_length": 519,
|
| 157 |
+
"ref_digit_count": 80,
|
| 158 |
"ref_cjk_count": 0
|
| 159 |
},
|
| 160 |
{
|
| 161 |
"file": "0007.png",
|
| 162 |
+
"category": "table_numeric",
|
| 163 |
+
"cer": 0.17539863325740318,
|
| 164 |
"wer": 1.5,
|
| 165 |
+
"digit_cer": 0.1875,
|
| 166 |
"cjk_cer": 0.0,
|
| 167 |
"repetition_rate": 0.0,
|
| 168 |
"table_score": 1.0,
|
| 169 |
+
"elapsed_seconds": 7.832311457998003,
|
| 170 |
+
"ref_length": 446,
|
| 171 |
+
"hyp_length": 519,
|
| 172 |
+
"ref_digit_count": 80,
|
| 173 |
"ref_cjk_count": 0
|
| 174 |
},
|
| 175 |
{
|
| 176 |
"file": "0008.png",
|
| 177 |
+
"category": "cjk_document",
|
| 178 |
+
"cer": 1.5730337078651686,
|
| 179 |
"wer": 0.9,
|
| 180 |
"digit_cer": 0.0,
|
| 181 |
"cjk_cer": 1.0,
|
| 182 |
"repetition_rate": 0.0,
|
| 183 |
"table_score": null,
|
| 184 |
+
"elapsed_seconds": 6.520762083993759,
|
| 185 |
+
"ref_length": 98,
|
| 186 |
+
"hyp_length": 193,
|
| 187 |
+
"ref_digit_count": 30,
|
| 188 |
"ref_cjk_count": 41
|
| 189 |
},
|
| 190 |
{
|
| 191 |
"file": "0009.png",
|
| 192 |
+
"category": "cjk_document",
|
| 193 |
+
"cer": 3.202247191011236,
|
| 194 |
"wer": 1.0,
|
| 195 |
"digit_cer": 0.0,
|
| 196 |
"cjk_cer": 1.0,
|
| 197 |
"repetition_rate": 0.0,
|
| 198 |
"table_score": null,
|
| 199 |
+
"elapsed_seconds": 6.874465999993845,
|
| 200 |
+
"ref_length": 100,
|
| 201 |
+
"hyp_length": 334,
|
| 202 |
+
"ref_digit_count": 30,
|
| 203 |
"ref_cjk_count": 42
|
| 204 |
},
|
| 205 |
{
|
| 206 |
"file": "0010.png",
|
| 207 |
+
"category": "cjk_document",
|
| 208 |
+
"cer": 2.1758241758241756,
|
| 209 |
"wer": 1.9545454545454546,
|
| 210 |
+
"digit_cer": 0.26666666666666666,
|
| 211 |
"cjk_cer": 1.0,
|
| 212 |
"repetition_rate": 0.0,
|
| 213 |
"table_score": null,
|
| 214 |
+
"elapsed_seconds": 6.385247083002469,
|
| 215 |
+
"ref_length": 112,
|
| 216 |
+
"hyp_length": 275,
|
| 217 |
+
"ref_digit_count": 30,
|
| 218 |
"ref_cjk_count": 44
|
| 219 |
},
|
| 220 |
{
|
| 221 |
"file": "0011.png",
|
| 222 |
+
"category": "low_quality_scan",
|
| 223 |
+
"cer": 0.12380952380952381,
|
| 224 |
+
"wer": 0.125,
|
| 225 |
+
"digit_cer": 0.27586206896551724,
|
| 226 |
"cjk_cer": 0.0,
|
| 227 |
"repetition_rate": 0.0,
|
| 228 |
"table_score": null,
|
| 229 |
+
"elapsed_seconds": 5.718108458007919,
|
| 230 |
+
"ref_length": 120,
|
| 231 |
+
"hyp_length": 135,
|
| 232 |
+
"ref_digit_count": 29,
|
| 233 |
"ref_cjk_count": 0
|
| 234 |
},
|
| 235 |
{
|
| 236 |
"file": "0012.png",
|
| 237 |
+
"category": "low_quality_scan",
|
| 238 |
+
"cer": 0.01904761904761905,
|
| 239 |
+
"wer": 0.0625,
|
| 240 |
"digit_cer": 0.0,
|
| 241 |
"cjk_cer": 0.0,
|
| 242 |
"repetition_rate": 0.0,
|
| 243 |
"table_score": null,
|
| 244 |
+
"elapsed_seconds": 5.730329083002289,
|
| 245 |
+
"ref_length": 120,
|
| 246 |
+
"hyp_length": 123,
|
| 247 |
+
"ref_digit_count": 29,
|
| 248 |
"ref_cjk_count": 0
|
| 249 |
}
|
| 250 |
]
|
release/reference_performance.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
|
|
|
| 3 |
"image_path": "test_invoice.png",
|
| 4 |
"prompt": "<image>document parsing.",
|
| 5 |
"max_tokens": 256,
|
|
@@ -13,55 +14,55 @@
|
|
| 13 |
"chip": "Apple M3 Max",
|
| 14 |
"total_memory_gb": 128.0
|
| 15 |
},
|
| 16 |
-
"model_load_time_seconds":
|
| 17 |
-
"memory_after_load_mb":
|
| 18 |
"num_warmup": 1,
|
| 19 |
"num_runs": 3,
|
| 20 |
-
"mean_tps":
|
| 21 |
-
"std_tps": 0.
|
| 22 |
-
"mean_elapsed_seconds":
|
| 23 |
"mean_peak_memory_mb": 5165.991419999999,
|
| 24 |
"runs": [
|
| 25 |
{
|
| 26 |
"run": 1,
|
| 27 |
-
"elapsed_seconds":
|
| 28 |
"tokens_generated": 256,
|
| 29 |
"tokens_generated_source": "mlx-vlm token count",
|
| 30 |
-
"tokens_per_second":
|
| 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":
|
| 35 |
"prompt_tokens": 697,
|
| 36 |
-
"prompt_tokens_per_second":
|
| 37 |
"finish_reason": "length"
|
| 38 |
},
|
| 39 |
{
|
| 40 |
"run": 2,
|
| 41 |
-
"elapsed_seconds":
|
| 42 |
"tokens_generated": 256,
|
| 43 |
"tokens_generated_source": "mlx-vlm token count",
|
| 44 |
-
"tokens_per_second":
|
| 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":
|
| 49 |
"prompt_tokens": 697,
|
| 50 |
-
"prompt_tokens_per_second":
|
| 51 |
"finish_reason": "length"
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"run": 3,
|
| 55 |
-
"elapsed_seconds":
|
| 56 |
"tokens_generated": 256,
|
| 57 |
"tokens_generated_source": "mlx-vlm token count",
|
| 58 |
-
"tokens_per_second":
|
| 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.
|
| 63 |
"prompt_tokens": 697,
|
| 64 |
-
"prompt_tokens_per_second":
|
| 65 |
"finish_reason": "length"
|
| 66 |
}
|
| 67 |
]
|
|
|
|
| 1 |
{
|
| 2 |
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 3 |
+
"served_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 4 |
"image_path": "test_invoice.png",
|
| 5 |
"prompt": "<image>document parsing.",
|
| 6 |
"max_tokens": 256,
|
|
|
|
| 14 |
"chip": "Apple M3 Max",
|
| 15 |
"total_memory_gb": 128.0
|
| 16 |
},
|
| 17 |
+
"model_load_time_seconds": 2.9305669579916866,
|
| 18 |
+
"memory_after_load_mb": 3992.859375,
|
| 19 |
"num_warmup": 1,
|
| 20 |
"num_runs": 3,
|
| 21 |
+
"mean_tps": 35.07146198590831,
|
| 22 |
+
"std_tps": 0.290807343078657,
|
| 23 |
+
"mean_elapsed_seconds": 8.953089208662277,
|
| 24 |
"mean_peak_memory_mb": 5165.991419999999,
|
| 25 |
"runs": [
|
| 26 |
{
|
| 27 |
"run": 1,
|
| 28 |
+
"elapsed_seconds": 9.042429541994352,
|
| 29 |
"tokens_generated": 256,
|
| 30 |
"tokens_generated_source": "mlx-vlm token count",
|
| 31 |
+
"tokens_per_second": 34.74497597308309,
|
| 32 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 33 |
"peak_memory_mb": 5165.81578,
|
| 34 |
"memory_delta_mb": 1337.186768,
|
| 35 |
+
"process_peak_rss_mb": 4022.6875,
|
| 36 |
"prompt_tokens": 697,
|
| 37 |
+
"prompt_tokens_per_second": 429.97806661114964,
|
| 38 |
"finish_reason": "length"
|
| 39 |
},
|
| 40 |
{
|
| 41 |
"run": 2,
|
| 42 |
+
"elapsed_seconds": 9.023512416999438,
|
| 43 |
"tokens_generated": 256,
|
| 44 |
"tokens_generated_source": "mlx-vlm token count",
|
| 45 |
+
"tokens_per_second": 35.01812122387363,
|
| 46 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 47 |
"peak_memory_mb": 5166.07924,
|
| 48 |
"memory_delta_mb": 1337.188084,
|
| 49 |
+
"process_peak_rss_mb": 4023.046875,
|
| 50 |
"prompt_tokens": 697,
|
| 51 |
+
"prompt_tokens_per_second": 417.40764558358615,
|
| 52 |
"finish_reason": "length"
|
| 53 |
},
|
| 54 |
{
|
| 55 |
"run": 3,
|
| 56 |
+
"elapsed_seconds": 8.793325666993042,
|
| 57 |
"tokens_generated": 256,
|
| 58 |
"tokens_generated_source": "mlx-vlm token count",
|
| 59 |
+
"tokens_per_second": 35.451288760768186,
|
| 60 |
"tokens_per_second_source": "mlx-vlm generation_tps",
|
| 61 |
"peak_memory_mb": 5166.07924,
|
| 62 |
"memory_delta_mb": 1337.188084,
|
| 63 |
+
"process_peak_rss_mb": 4023.3125,
|
| 64 |
"prompt_tokens": 697,
|
| 65 |
+
"prompt_tokens_per_second": 449.69288066410945,
|
| 66 |
"finish_reason": "length"
|
| 67 |
}
|
| 68 |
]
|
release/release_manifest.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
{
|
| 2 |
-
"schema_version":
|
| 3 |
-
"created_at": "2026-07-
|
| 4 |
"release_approved": true,
|
| 5 |
"repo_id": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 6 |
"source": {
|
| 7 |
"id": "baidu/Unlimited-OCR",
|
|
|
|
| 8 |
"weights": {
|
| 9 |
"files": [
|
| 10 |
{
|
|
@@ -20,6 +21,7 @@
|
|
| 20 |
},
|
| 21 |
"reference": {
|
| 22 |
"id": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
|
|
|
| 23 |
"weights": {
|
| 24 |
"files": [
|
| 25 |
{
|
|
@@ -39,13 +41,63 @@
|
|
| 39 |
"files": [
|
| 40 |
{
|
| 41 |
"name": "model.safetensors",
|
| 42 |
-
"size":
|
| 43 |
-
"sha256": "
|
| 44 |
}
|
| 45 |
],
|
| 46 |
-
"total_size_bytes":
|
| 47 |
-
"total_size_gb": 3.
|
| 48 |
-
"aggregate_sha256": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
},
|
| 50 |
"metadata": {
|
| 51 |
"passed": true,
|
|
@@ -55,25 +107,210 @@
|
|
| 55 |
"mxfp8": true,
|
| 56 |
"sliding_window": true,
|
| 57 |
"processor_class": true,
|
| 58 |
-
"sft_format": true
|
|
|
|
|
|
|
|
|
|
| 59 |
},
|
| 60 |
-
"sliding_window": 128
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
},
|
| 63 |
-
"
|
| 64 |
-
"
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
"num_cjk_samples": 3,
|
| 68 |
"num_table_samples": 3,
|
| 69 |
"category_counts": {
|
| 70 |
"cjk_document": 3,
|
| 71 |
-
"english_document":
|
| 72 |
"low_quality_scan": 2,
|
| 73 |
"table_numeric": 3
|
| 74 |
},
|
| 75 |
-
"manifest_sha256": "
|
| 76 |
-
"content_sha256": "
|
| 77 |
"files": [
|
| 78 |
{
|
| 79 |
"path": "images/0001.png",
|
|
@@ -97,120 +334,239 @@
|
|
| 97 |
},
|
| 98 |
{
|
| 99 |
"path": "images/0003.png",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
"size": 181040,
|
| 101 |
"sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
|
| 102 |
},
|
| 103 |
{
|
| 104 |
-
"path": "ground_truth/
|
| 105 |
"size": 1333,
|
| 106 |
"sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
|
| 107 |
},
|
| 108 |
{
|
| 109 |
-
"path": "images/
|
| 110 |
"size": 220208,
|
| 111 |
"sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
|
| 112 |
},
|
| 113 |
{
|
| 114 |
-
"path": "ground_truth/
|
| 115 |
"size": 1330,
|
| 116 |
"sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
|
| 117 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
{
|
| 119 |
"path": "images/0005.png",
|
| 120 |
-
"size":
|
| 121 |
-
"sha256": "
|
| 122 |
},
|
| 123 |
{
|
| 124 |
"path": "ground_truth/0005.txt",
|
| 125 |
-
"size":
|
| 126 |
-
"sha256": "
|
| 127 |
},
|
| 128 |
{
|
| 129 |
"path": "images/0006.png",
|
| 130 |
-
"size":
|
| 131 |
-
"sha256": "
|
| 132 |
},
|
| 133 |
{
|
| 134 |
"path": "ground_truth/0006.txt",
|
| 135 |
-
"size":
|
| 136 |
-
"sha256": "
|
| 137 |
},
|
| 138 |
{
|
| 139 |
"path": "images/0007.png",
|
| 140 |
-
"size":
|
| 141 |
-
"sha256": "
|
| 142 |
},
|
| 143 |
{
|
| 144 |
"path": "ground_truth/0007.txt",
|
| 145 |
-
"size":
|
| 146 |
-
"sha256": "
|
| 147 |
},
|
| 148 |
{
|
| 149 |
"path": "images/0008.png",
|
| 150 |
-
"size":
|
| 151 |
-
"sha256": "
|
| 152 |
},
|
| 153 |
{
|
| 154 |
"path": "ground_truth/0008.txt",
|
| 155 |
-
"size":
|
| 156 |
-
"sha256": "
|
| 157 |
},
|
| 158 |
{
|
| 159 |
"path": "images/0009.png",
|
| 160 |
-
"size":
|
| 161 |
-
"sha256": "
|
| 162 |
},
|
| 163 |
{
|
| 164 |
"path": "ground_truth/0009.txt",
|
| 165 |
-
"size":
|
| 166 |
-
"sha256": "
|
| 167 |
},
|
| 168 |
{
|
| 169 |
"path": "images/0010.png",
|
| 170 |
-
"size":
|
| 171 |
-
"sha256": "
|
| 172 |
},
|
| 173 |
{
|
| 174 |
"path": "ground_truth/0010.txt",
|
| 175 |
-
"size":
|
| 176 |
-
"sha256": "
|
| 177 |
},
|
| 178 |
{
|
| 179 |
"path": "images/0011.png",
|
| 180 |
-
"size":
|
| 181 |
-
"sha256": "
|
| 182 |
},
|
| 183 |
{
|
| 184 |
"path": "ground_truth/0011.txt",
|
| 185 |
-
"size":
|
| 186 |
-
"sha256": "
|
| 187 |
},
|
| 188 |
{
|
| 189 |
"path": "images/0012.png",
|
| 190 |
-
"size":
|
| 191 |
-
"sha256": "
|
| 192 |
},
|
| 193 |
{
|
| 194 |
"path": "ground_truth/0012.txt",
|
| 195 |
-
"size":
|
| 196 |
-
"sha256": "
|
| 197 |
}
|
| 198 |
]
|
| 199 |
},
|
| 200 |
"thresholds": {
|
| 201 |
-
"max_cer_delta_vs_bf16": 0.
|
| 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.
|
| 213 |
-
"mean_digit_cer": 0.
|
| 214 |
"mean_cjk_cer": 0.3333333333333333,
|
| 215 |
"mean_table_score": 1.0,
|
| 216 |
"max_tokens": 1024,
|
|
@@ -219,8 +575,8 @@
|
|
| 219 |
"reference_accuracy": {
|
| 220 |
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 221 |
"num_samples": 12,
|
| 222 |
-
"mean_cer":
|
| 223 |
-
"mean_digit_cer": 0.
|
| 224 |
"mean_cjk_cer": 1.0,
|
| 225 |
"mean_table_score": 1.0,
|
| 226 |
"max_tokens": 1024,
|
|
@@ -229,23 +585,23 @@
|
|
| 229 |
"candidate_accuracy": {
|
| 230 |
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 231 |
"num_samples": 12,
|
| 232 |
-
"mean_cer": 0.
|
| 233 |
-
"mean_digit_cer": 0.
|
| 234 |
-
"mean_cjk_cer": 0.
|
| 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":
|
| 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":
|
| 248 |
-
"mean_peak_memory_mb":
|
| 249 |
"max_tokens": 256
|
| 250 |
}
|
| 251 |
},
|
|
@@ -254,23 +610,28 @@
|
|
| 254 |
"image_path": "test_invoice.png",
|
| 255 |
"prompt": "<image>document parsing.",
|
| 256 |
"force_min_tokens": 8192,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
"forced_eos_token_ids": [
|
| 258 |
1
|
| 259 |
],
|
| 260 |
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 261 |
"processor": "arm",
|
| 262 |
-
"baseline_memory_mb":
|
| 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":
|
| 270 |
-
"mean_tps":
|
| 271 |
"tps_source": "mlx-vlm generation_tps",
|
| 272 |
-
"peak_memory_mb":
|
| 273 |
-
"memory_growth_mb": 1337.
|
| 274 |
"repetition_rate": 0.0,
|
| 275 |
"completed_naturally": false,
|
| 276 |
"finish_reason": "length",
|
|
@@ -282,12 +643,12 @@
|
|
| 282 |
"tokens_generated": 2048,
|
| 283 |
"tokens_generated_source": "mlx-vlm token count",
|
| 284 |
"tokens_generated_reliable": true,
|
| 285 |
-
"elapsed_seconds":
|
| 286 |
-
"mean_tps":
|
| 287 |
"tps_source": "mlx-vlm generation_tps",
|
| 288 |
-
"peak_memory_mb":
|
| 289 |
-
"memory_growth_mb": 1337.
|
| 290 |
-
"repetition_rate": 0.
|
| 291 |
"completed_naturally": false,
|
| 292 |
"finish_reason": "length",
|
| 293 |
"forced_minimum_tokens": 2048,
|
|
@@ -298,12 +659,12 @@
|
|
| 298 |
"tokens_generated": 8192,
|
| 299 |
"tokens_generated_source": "mlx-vlm token count",
|
| 300 |
"tokens_generated_reliable": true,
|
| 301 |
-
"elapsed_seconds":
|
| 302 |
-
"mean_tps":
|
| 303 |
"tps_source": "mlx-vlm generation_tps",
|
| 304 |
-
"peak_memory_mb":
|
| 305 |
-
"memory_growth_mb": 1337.
|
| 306 |
-
"repetition_rate": 0.
|
| 307 |
"completed_naturally": false,
|
| 308 |
"finish_reason": "length",
|
| 309 |
"forced_minimum_tokens": 8192,
|
|
@@ -315,7 +676,7 @@
|
|
| 315 |
"cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
|
| 316 |
"tps_stable_over_length": true,
|
| 317 |
"max_successful_tokens": 8192,
|
| 318 |
-
"max_repetition_rate": 0.
|
| 319 |
},
|
| 320 |
"pass_conditions": {
|
| 321 |
"cache_bounded": true,
|
|
@@ -326,14 +687,14 @@
|
|
| 326 |
},
|
| 327 |
"sensitivity": {
|
| 328 |
"baseline": {
|
| 329 |
-
"num_samples":
|
| 330 |
-
"num_digit_samples":
|
| 331 |
"num_table_samples": 3,
|
| 332 |
-
"mean_cer": 0.
|
| 333 |
-
"mean_digit_cer": 0.
|
| 334 |
"mean_table_score": 1.0,
|
| 335 |
-
"total_inference_time":
|
| 336 |
-
"avg_time_per_image":
|
| 337 |
},
|
| 338 |
"groups": {
|
| 339 |
"vision_encoder": {
|
|
@@ -487,17 +848,17 @@
|
|
| 487 |
"vision_model.transformer.layers.9.self_attn.qkv_proj"
|
| 488 |
],
|
| 489 |
"metrics": {
|
| 490 |
-
"num_samples":
|
| 491 |
-
"num_digit_samples":
|
| 492 |
"num_table_samples": 3,
|
| 493 |
-
"mean_cer": 2.
|
| 494 |
-
"mean_digit_cer": 1.
|
| 495 |
"mean_table_score": 0.0,
|
| 496 |
-
"total_inference_time":
|
| 497 |
-
"avg_time_per_image":
|
| 498 |
},
|
| 499 |
-
"cer_delta": 2.
|
| 500 |
-
"digit_cer_delta": 1.
|
| 501 |
"table_score_degradation": 1.0,
|
| 502 |
"sensitivity_rank": 1
|
| 503 |
},
|
|
@@ -508,19 +869,19 @@
|
|
| 508 |
"projector.layers"
|
| 509 |
],
|
| 510 |
"metrics": {
|
| 511 |
-
"num_samples":
|
| 512 |
-
"num_digit_samples":
|
| 513 |
"num_table_samples": 3,
|
| 514 |
-
"mean_cer": 0.
|
| 515 |
-
"mean_digit_cer": 0.
|
| 516 |
"mean_table_score": 1.0,
|
| 517 |
-
"total_inference_time":
|
| 518 |
-
"avg_time_per_image":
|
| 519 |
},
|
| 520 |
-
"cer_delta":
|
| 521 |
-
"digit_cer_delta":
|
| 522 |
"table_score_degradation": 0.0,
|
| 523 |
-
"sensitivity_rank":
|
| 524 |
},
|
| 525 |
"token_embeddings": {
|
| 526 |
"status": "success",
|
|
@@ -529,17 +890,17 @@
|
|
| 529 |
"language_model.model.embed_tokens"
|
| 530 |
],
|
| 531 |
"metrics": {
|
| 532 |
-
"num_samples":
|
| 533 |
-
"num_digit_samples":
|
| 534 |
"num_table_samples": 3,
|
| 535 |
-
"mean_cer": 0.
|
| 536 |
-
"mean_digit_cer": 0.
|
| 537 |
"mean_table_score": 1.0,
|
| 538 |
-
"total_inference_time":
|
| 539 |
-
"avg_time_per_image":
|
| 540 |
},
|
| 541 |
-
"cer_delta": -0.
|
| 542 |
-
"digit_cer_delta": -0.
|
| 543 |
"table_score_degradation": 0.0,
|
| 544 |
"sensitivity_rank": 10
|
| 545 |
},
|
|
@@ -561,17 +922,17 @@
|
|
| 561 |
"language_model.model.layers.9.self_attn.q_proj"
|
| 562 |
],
|
| 563 |
"metrics": {
|
| 564 |
-
"num_samples":
|
| 565 |
-
"num_digit_samples":
|
| 566 |
"num_table_samples": 3,
|
| 567 |
-
"mean_cer": 0.
|
| 568 |
-
"mean_digit_cer": 0.
|
| 569 |
"mean_table_score": 1.0,
|
| 570 |
-
"total_inference_time":
|
| 571 |
-
"avg_time_per_image":
|
| 572 |
},
|
| 573 |
-
"cer_delta": -0.
|
| 574 |
-
"digit_cer_delta": 0.
|
| 575 |
"table_score_degradation": 0.0,
|
| 576 |
"sensitivity_rank": 11
|
| 577 |
},
|
|
@@ -593,17 +954,17 @@
|
|
| 593 |
"language_model.model.layers.9.self_attn.k_proj"
|
| 594 |
],
|
| 595 |
"metrics": {
|
| 596 |
-
"num_samples":
|
| 597 |
-
"num_digit_samples":
|
| 598 |
"num_table_samples": 3,
|
| 599 |
-
"mean_cer": 0.
|
| 600 |
-
"mean_digit_cer": 0.
|
| 601 |
"mean_table_score": 1.0,
|
| 602 |
-
"total_inference_time":
|
| 603 |
-
"avg_time_per_image":
|
| 604 |
},
|
| 605 |
-
"cer_delta": -0.
|
| 606 |
-
"digit_cer_delta":
|
| 607 |
"table_score_degradation": 0.0,
|
| 608 |
"sensitivity_rank": 9
|
| 609 |
},
|
|
@@ -625,19 +986,19 @@
|
|
| 625 |
"language_model.model.layers.9.self_attn.v_proj"
|
| 626 |
],
|
| 627 |
"metrics": {
|
| 628 |
-
"num_samples":
|
| 629 |
-
"num_digit_samples":
|
| 630 |
"num_table_samples": 3,
|
| 631 |
-
"mean_cer": 0.
|
| 632 |
-
"mean_digit_cer": 0.
|
| 633 |
"mean_table_score": 1.0,
|
| 634 |
-
"total_inference_time":
|
| 635 |
-
"avg_time_per_image":
|
| 636 |
},
|
| 637 |
-
"cer_delta": -0.
|
| 638 |
-
"digit_cer_delta":
|
| 639 |
"table_score_degradation": 0.0,
|
| 640 |
-
"sensitivity_rank":
|
| 641 |
},
|
| 642 |
"attention_o_proj": {
|
| 643 |
"status": "success",
|
|
@@ -657,19 +1018,19 @@
|
|
| 657 |
"language_model.model.layers.9.self_attn.o_proj"
|
| 658 |
],
|
| 659 |
"metrics": {
|
| 660 |
-
"num_samples":
|
| 661 |
-
"num_digit_samples":
|
| 662 |
"num_table_samples": 3,
|
| 663 |
-
"mean_cer": 0.
|
| 664 |
-
"mean_digit_cer": 0.
|
| 665 |
"mean_table_score": 1.0,
|
| 666 |
-
"total_inference_time":
|
| 667 |
-
"avg_time_per_image":
|
| 668 |
},
|
| 669 |
-
"cer_delta":
|
| 670 |
-
"digit_cer_delta": -0.
|
| 671 |
"table_score_degradation": 0.0,
|
| 672 |
-
"sensitivity_rank":
|
| 673 |
},
|
| 674 |
"dense_mlp": {
|
| 675 |
"status": "success",
|
|
@@ -680,19 +1041,19 @@
|
|
| 680 |
"language_model.model.layers.0.mlp.up_proj"
|
| 681 |
],
|
| 682 |
"metrics": {
|
| 683 |
-
"num_samples":
|
| 684 |
-
"num_digit_samples":
|
| 685 |
"num_table_samples": 3,
|
| 686 |
-
"mean_cer": 0.
|
| 687 |
-
"mean_digit_cer": 0.
|
| 688 |
"mean_table_score": 1.0,
|
| 689 |
-
"total_inference_time":
|
| 690 |
-
"avg_time_per_image":
|
| 691 |
},
|
| 692 |
-
"cer_delta": -0.
|
| 693 |
-
"digit_cer_delta":
|
| 694 |
"table_score_degradation": 0.0,
|
| 695 |
-
"sensitivity_rank":
|
| 696 |
},
|
| 697 |
"shared_experts": {
|
| 698 |
"status": "success",
|
|
@@ -733,19 +1094,19 @@
|
|
| 733 |
"language_model.model.layers.9.mlp.shared_experts.up_proj"
|
| 734 |
],
|
| 735 |
"metrics": {
|
| 736 |
-
"num_samples":
|
| 737 |
-
"num_digit_samples":
|
| 738 |
"num_table_samples": 3,
|
| 739 |
-
"mean_cer": 0.
|
| 740 |
-
"mean_digit_cer": 0.
|
| 741 |
"mean_table_score": 1.0,
|
| 742 |
-
"total_inference_time":
|
| 743 |
-
"avg_time_per_image":
|
| 744 |
},
|
| 745 |
-
"cer_delta": -0.
|
| 746 |
-
"digit_cer_delta": 0.
|
| 747 |
"table_score_degradation": 0.0,
|
| 748 |
-
"sensitivity_rank":
|
| 749 |
},
|
| 750 |
"routed_experts": {
|
| 751 |
"status": "success",
|
|
@@ -786,19 +1147,19 @@
|
|
| 786 |
"language_model.model.layers.9.mlp.switch_mlp.up_proj"
|
| 787 |
],
|
| 788 |
"metrics": {
|
| 789 |
-
"num_samples":
|
| 790 |
-
"num_digit_samples":
|
| 791 |
"num_table_samples": 3,
|
| 792 |
-
"mean_cer": 0.
|
| 793 |
-
"mean_digit_cer": 0.
|
| 794 |
"mean_table_score": 1.0,
|
| 795 |
-
"total_inference_time":
|
| 796 |
-
"avg_time_per_image":
|
| 797 |
},
|
| 798 |
-
"cer_delta":
|
| 799 |
-
"digit_cer_delta": -0.
|
| 800 |
"table_score_degradation": 0.0,
|
| 801 |
-
"sensitivity_rank":
|
| 802 |
},
|
| 803 |
"lm_head": {
|
| 804 |
"status": "success",
|
|
@@ -807,57 +1168,59 @@
|
|
| 807 |
"language_model.lm_head"
|
| 808 |
],
|
| 809 |
"metrics": {
|
| 810 |
-
"num_samples":
|
| 811 |
-
"num_digit_samples":
|
| 812 |
"num_table_samples": 3,
|
| 813 |
-
"mean_cer": 0.
|
| 814 |
-
"mean_digit_cer": 0.
|
| 815 |
"mean_table_score": 1.0,
|
| 816 |
-
"total_inference_time":
|
| 817 |
-
"avg_time_per_image":
|
| 818 |
},
|
| 819 |
-
"cer_delta": 0.
|
| 820 |
-
"digit_cer_delta": 0.
|
| 821 |
"table_score_degradation": 0.0,
|
| 822 |
-
"sensitivity_rank":
|
| 823 |
}
|
| 824 |
}
|
| 825 |
},
|
| 826 |
"calibration": {
|
| 827 |
-
"schema_version":
|
| 828 |
-
"created_at": "2026-07-
|
| 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.
|
| 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":
|
| 844 |
"checks": {
|
| 845 |
"cer": true,
|
| 846 |
"digit_cer": true,
|
| 847 |
"table_score": true,
|
| 848 |
-
"throughput":
|
| 849 |
},
|
| 850 |
"metrics": {
|
| 851 |
-
"mean_cer": 0.
|
| 852 |
-
"mean_digit_cer": 0.
|
| 853 |
"mean_table_score": 1.0,
|
| 854 |
-
"mean_tps":
|
| 855 |
},
|
| 856 |
"deltas": {
|
| 857 |
-
"cer_vs_bf16": -0.
|
| 858 |
-
"digit_cer_vs_bf16": -0.
|
| 859 |
"table_degradation_vs_bf16": 0.0,
|
| 860 |
-
"tps_ratio_vs_reference": 0.
|
| 861 |
}
|
| 862 |
},
|
| 863 |
{
|
|
@@ -865,22 +1228,22 @@
|
|
| 865 |
"precision": "mxfp8",
|
| 866 |
"passed": false,
|
| 867 |
"checks": {
|
| 868 |
-
"cer":
|
| 869 |
-
"digit_cer":
|
| 870 |
"table_score": true,
|
| 871 |
"throughput": true
|
| 872 |
},
|
| 873 |
"metrics": {
|
| 874 |
-
"mean_cer": 0.
|
| 875 |
-
"mean_digit_cer": 0.
|
| 876 |
"mean_table_score": 1.0,
|
| 877 |
-
"mean_tps":
|
| 878 |
},
|
| 879 |
"deltas": {
|
| 880 |
-
"cer_vs_bf16": 0.
|
| 881 |
-
"digit_cer_vs_bf16":
|
| 882 |
"table_degradation_vs_bf16": 0.0,
|
| 883 |
-
"tps_ratio_vs_reference":
|
| 884 |
}
|
| 885 |
},
|
| 886 |
{
|
|
@@ -894,16 +1257,16 @@
|
|
| 894 |
"throughput": true
|
| 895 |
},
|
| 896 |
"metrics": {
|
| 897 |
-
"mean_cer": 0.
|
| 898 |
-
"mean_digit_cer": 0.
|
| 899 |
"mean_table_score": 1.0,
|
| 900 |
-
"mean_tps":
|
| 901 |
},
|
| 902 |
"deltas": {
|
| 903 |
-
"cer_vs_bf16": -0.
|
| 904 |
-
"digit_cer_vs_bf16":
|
| 905 |
"table_degradation_vs_bf16": 0.0,
|
| 906 |
-
"tps_ratio_vs_reference": 0.
|
| 907 |
}
|
| 908 |
}
|
| 909 |
],
|
|
@@ -913,11 +1276,170 @@
|
|
| 913 |
},
|
| 914 |
"precision_overrides": {
|
| 915 |
"language_model.lm_head": "affine8"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
}
|
| 917 |
},
|
| 918 |
"precision_map": {
|
| 919 |
"vision_model": "bfloat16",
|
| 920 |
-
"projector": "
|
| 921 |
"language_model.model.embed_tokens": "mxfp8",
|
| 922 |
"language_model.model.norm": "bfloat16",
|
| 923 |
"language_model.lm_head": "affine8",
|
|
@@ -949,16 +1471,16 @@
|
|
| 949 |
"vision_encoder": {
|
| 950 |
"precision": "bfloat16",
|
| 951 |
"reasons": [
|
| 952 |
-
"cer_delta=2.
|
| 953 |
-
"digit_cer_delta=1.
|
| 954 |
"table_score_degradation=1.000000",
|
| 955 |
"protected OCR-critical group"
|
| 956 |
]
|
| 957 |
},
|
| 958 |
"vision_projector": {
|
| 959 |
-
"precision": "
|
| 960 |
"reasons": [
|
| 961 |
-
"all measured quality deltas
|
| 962 |
]
|
| 963 |
},
|
| 964 |
"token_embeddings": {
|
|
@@ -1026,69 +1548,109 @@
|
|
| 1026 |
}
|
| 1027 |
},
|
| 1028 |
"artifacts": {
|
| 1029 |
-
"bf16_accuracy": {
|
| 1030 |
-
"filename": "bf16_accuracy.json",
|
| 1031 |
-
"size": 4857,
|
| 1032 |
-
"sha256": "0de8511f1ba98f362d3ab712522115e534d7f129a8b587751ba5d5d8c95f272d"
|
| 1033 |
-
},
|
| 1034 |
"reference_accuracy": {
|
| 1035 |
"filename": "reference_accuracy.json",
|
| 1036 |
-
"size":
|
| 1037 |
-
"sha256": "
|
| 1038 |
},
|
| 1039 |
-
"
|
| 1040 |
-
"filename": "
|
| 1041 |
-
"size":
|
| 1042 |
-
"sha256": "
|
| 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":
|
| 1052 |
-
"sha256": "
|
| 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":
|
| 1062 |
-
"sha256": "
|
| 1063 |
},
|
| 1064 |
"calibration_results": {
|
| 1065 |
"filename": "calibration_results.json",
|
| 1066 |
-
"size":
|
| 1067 |
-
"sha256": "
|
| 1068 |
},
|
| 1069 |
-
"
|
| 1070 |
-
"filename": "
|
| 1071 |
-
"size":
|
| 1072 |
-
"sha256": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
},
|
| 1074 |
"provenance": {
|
| 1075 |
"filename": "provenance.json",
|
| 1076 |
-
"size":
|
| 1077 |
-
"sha256": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1078 |
}
|
| 1079 |
},
|
| 1080 |
"gates": [
|
| 1081 |
{
|
| 1082 |
"name": "weights_are_distinct",
|
| 1083 |
"passed": true,
|
| 1084 |
-
"actual": "
|
| 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.
|
| 1092 |
"limit": 4.5,
|
| 1093 |
"detail": "Candidate Safetensors size"
|
| 1094 |
},
|
|
@@ -1101,11 +1663,30 @@
|
|
| 1101 |
"mxfp8": true,
|
| 1102 |
"sliding_window": true,
|
| 1103 |
"processor_class": true,
|
| 1104 |
-
"sft_format": true
|
|
|
|
|
|
|
|
|
|
| 1105 |
},
|
| 1106 |
"limit": true,
|
| 1107 |
"detail": "Native Unlimited-OCR, MXFP8, and R-SWA metadata"
|
| 1108 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1109 |
{
|
| 1110 |
"name": "evaluation_coverage",
|
| 1111 |
"passed": true,
|
|
@@ -1119,34 +1700,168 @@
|
|
| 1119 |
"detail": "Dataset must cover ordinary text, digits, CJK, and tables"
|
| 1120 |
},
|
| 1121 |
{
|
| 1122 |
-
"name": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1123 |
"passed": true,
|
| 1124 |
"actual": [
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1128 |
],
|
| 1129 |
-
"
|
| 1130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1131 |
},
|
| 1132 |
{
|
| 1133 |
"name": "candidate_cer_vs_bf16",
|
| 1134 |
"passed": true,
|
| 1135 |
-
"actual":
|
| 1136 |
-
"limit": 0.
|
| 1137 |
"detail": "Candidate minus BF16 absolute mean CER"
|
| 1138 |
},
|
| 1139 |
{
|
| 1140 |
"name": "candidate_cer_vs_reference",
|
| 1141 |
"passed": true,
|
| 1142 |
-
"actual": -0.
|
| 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.
|
| 1150 |
"limit": 0.01,
|
| 1151 |
"detail": "Candidate minus BF16 digit CER"
|
| 1152 |
},
|
|
@@ -1157,10 +1872,73 @@
|
|
| 1157 |
"limit": 0.01,
|
| 1158 |
"detail": "BF16 minus candidate mean table score"
|
| 1159 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1160 |
{
|
| 1161 |
"name": "candidate_tps_vs_reference",
|
| 1162 |
"passed": true,
|
| 1163 |
-
"actual":
|
| 1164 |
"limit": 0.9,
|
| 1165 |
"detail": "Candidate decode throughput divided by Sahil-reference throughput"
|
| 1166 |
},
|
|
@@ -1168,12 +1946,134 @@
|
|
| 1168 |
"name": "rswa_8k_bounded",
|
| 1169 |
"passed": true,
|
| 1170 |
"actual": {
|
| 1171 |
-
"
|
| 1172 |
-
|
| 1173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
},
|
| 1175 |
-
"limit":
|
| 1176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1177 |
}
|
| 1178 |
],
|
| 1179 |
"environment": {
|
|
|
|
| 1 |
{
|
| 2 |
+
"schema_version": 3,
|
| 3 |
+
"created_at": "2026-07-21T05:26:20.621136+00:00",
|
| 4 |
"release_approved": true,
|
| 5 |
"repo_id": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 6 |
"source": {
|
| 7 |
"id": "baidu/Unlimited-OCR",
|
| 8 |
+
"resolved_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 9 |
"weights": {
|
| 10 |
"files": [
|
| 11 |
{
|
|
|
|
| 21 |
},
|
| 22 |
"reference": {
|
| 23 |
"id": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 24 |
+
"resolved_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 25 |
"weights": {
|
| 26 |
"files": [
|
| 27 |
{
|
|
|
|
| 41 |
"files": [
|
| 42 |
{
|
| 43 |
"name": "model.safetensors",
|
| 44 |
+
"size": 3845699993,
|
| 45 |
+
"sha256": "d9fe11fc8d2e333000be6a0e288681231efa69282c844f7775ca40f52db02f0c"
|
| 46 |
}
|
| 47 |
],
|
| 48 |
+
"total_size_bytes": 3845699993,
|
| 49 |
+
"total_size_gb": 3.5815872186794877,
|
| 50 |
+
"aggregate_sha256": "5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549"
|
| 51 |
+
},
|
| 52 |
+
"files": {
|
| 53 |
+
"files": [
|
| 54 |
+
{
|
| 55 |
+
"path": "config.json",
|
| 56 |
+
"size": 33815,
|
| 57 |
+
"sha256": "2c3eeb61157f6b9a5c42aa0771771911516e3ac8b3cbd9b61d1532a33e84a2bd"
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"path": "model.safetensors.index.json",
|
| 61 |
+
"size": 61701,
|
| 62 |
+
"sha256": "bb6693a47e22e596139e90d8a1442a64155900d27aa0e2419ec071ef4f0ad4a3"
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"path": "processor_config.json",
|
| 66 |
+
"size": 454,
|
| 67 |
+
"sha256": "e9f224a7f0d2263bf62efc052e93681720e31f9e34b1c42134d2371e8708a426"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"path": "special_tokens_map.json",
|
| 71 |
+
"size": 801,
|
| 72 |
+
"sha256": "ab4bd57ce17d62e39e0a39e739de1e407484f090f0b2c7e391312bca7a5b061a"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"path": "tokenizer.json",
|
| 76 |
+
"size": 9979544,
|
| 77 |
+
"sha256": "a02f8fd5228c90256bb4f6554c34a579d48f909e5beb232dc4afad870b55a8b4"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"path": "tokenizer_config.json",
|
| 81 |
+
"size": 569,
|
| 82 |
+
"sha256": "86f9fcd3f04060ba47a616241f5fc3f65b53a54ee7260367089467cdca849568"
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"path": "chat_template.jinja",
|
| 86 |
+
"size": 191,
|
| 87 |
+
"sha256": "b4e4771e69892ebc712ed6986bcce490c65041d1545e85599cd4a531cfab98a5"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"path": "precision_map.json",
|
| 91 |
+
"size": 3337,
|
| 92 |
+
"sha256": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"path": "quantization_summary.json",
|
| 96 |
+
"size": 15572,
|
| 97 |
+
"sha256": "417dbf5b59f6064361f6096cbf59bbe1774e7562301a49f47100fe4ceb201c55"
|
| 98 |
+
}
|
| 99 |
+
],
|
| 100 |
+
"aggregate_sha256": "cbd09caa91f51dd7abf2ebb16eb38d20a71ff00e21056a985f8bfd28269ac97c"
|
| 101 |
},
|
| 102 |
"metadata": {
|
| 103 |
"passed": true,
|
|
|
|
| 107 |
"mxfp8": true,
|
| 108 |
"sliding_window": true,
|
| 109 |
"processor_class": true,
|
| 110 |
+
"sft_format": true,
|
| 111 |
+
"precision_map": true,
|
| 112 |
+
"quantization_summary": true,
|
| 113 |
+
"protected_modules_preserved": true
|
| 114 |
},
|
| 115 |
+
"sliding_window": 128,
|
| 116 |
+
"source_model": "Unlimited-OCR",
|
| 117 |
+
"source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead"
|
| 118 |
}
|
| 119 |
},
|
| 120 |
+
"project_files": {
|
| 121 |
+
"files": [
|
| 122 |
+
{
|
| 123 |
+
"path": "LICENSE",
|
| 124 |
+
"size": 1067,
|
| 125 |
+
"sha256": "6268bc8a51423daeb65e5b0225d9a73fd87b464e235bebc02ceaccc1d1779f45"
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"path": "pyproject.toml",
|
| 129 |
+
"size": 2060,
|
| 130 |
+
"sha256": "7441c069b03a90e26b1997e94d52edb2d8a7ce0d5c012093080d143e79d19ba2"
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"path": "generation_config.json",
|
| 134 |
+
"size": 802,
|
| 135 |
+
"sha256": "69ac71fd1613d81cb83d07fb328ad6bf24eab5584fd5348665dad2497782954d"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"path": "preprocessing_config.json",
|
| 139 |
+
"size": 3033,
|
| 140 |
+
"sha256": "a269fce81f50bdabec9f7fac0219ccd9de62e2b7fd9d8a31f313b04e52b70432"
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"path": "quantization_config.json",
|
| 144 |
+
"size": 1441,
|
| 145 |
+
"sha256": "cc5d461002cece610848d697aa293a41b57edcebd93020348451dbe4de1d2481"
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"path": "docs/PRD.md",
|
| 149 |
+
"size": 6915,
|
| 150 |
+
"sha256": "c64527daf7d9b591a453be856d8a8733f8dd23f8a8e4c913cf47b49ff17786c1"
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"path": "docs/TECHNICAL_SPEC.md",
|
| 154 |
+
"size": 10379,
|
| 155 |
+
"sha256": "3967f46ee59728e75563907ec8f0e93d08a5da320d28395c4e3084a1ff68a3db"
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"path": "docs/adr/0001-verified-ocr-aware-quantization.md",
|
| 159 |
+
"size": 3312,
|
| 160 |
+
"sha256": "e5c4996d80263bcaf43a60cd91db45c214873486eee858fe0ac3f4762fde5e21"
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"path": "docs/adr/0002-calibrated-affine8-lm-head.md",
|
| 164 |
+
"size": 2722,
|
| 165 |
+
"sha256": "4606afa17413791e4bd4acc2e959162fc98e6978a67b236d201add396e31d5ea"
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"path": "src/unlimited_ocr/__init__.py",
|
| 169 |
+
"size": 533,
|
| 170 |
+
"sha256": "5f46da23ba70ec43fadd5e3b4b5f9335063e4fc86e2ef20d23e33f8722fb8d19"
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"path": "src/unlimited_ocr/engine.py",
|
| 174 |
+
"size": 24024,
|
| 175 |
+
"sha256": "b66e97254d317bf4b8886daa3d01aecae71237590d6bb249af08fd6c9831da1c"
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"path": "src/unlimited_ocr/pipeline.py",
|
| 179 |
+
"size": 26762,
|
| 180 |
+
"sha256": "baf259a4e46282b031cbdc71d1538dc68c2293935ff3581233d3d0b41e32d666"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"path": "src/unlimited_ocr/preprocessing.py",
|
| 184 |
+
"size": 28532,
|
| 185 |
+
"sha256": "d553b667a1c83ad1aedf63fdf642227a829dab66c705562e1c9d718f3ffe898d"
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"path": "src/unlimited_ocr/output.py",
|
| 189 |
+
"size": 15061,
|
| 190 |
+
"sha256": "c0f42a4c454ad39ef62714575d8f0c391ba5572edb9a79b9d06c7049382964df"
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"path": "src/unlimited_ocr/pdf.py",
|
| 194 |
+
"size": 8659,
|
| 195 |
+
"sha256": "3c23ea843bd06aa0712aabdf7c164270c8d363e32271a4e13a446ffdec05a460"
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"path": "src/unlimited_ocr/cli.py",
|
| 199 |
+
"size": 20469,
|
| 200 |
+
"sha256": "c6b813ef2a01a960369bbff79d270cb335f41b92ad3435b24cf2665cc2d2c6f5"
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"path": "src/unlimited_ocr/profiles.py",
|
| 204 |
+
"size": 2917,
|
| 205 |
+
"sha256": "ce31200aeb9c8e459b258e3258c0ded76ca40de0d78d07b8cb6bdd05ed45653f"
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"path": "src/unlimited_ocr/server.py",
|
| 209 |
+
"size": 12153,
|
| 210 |
+
"sha256": "7723c3931e610815efc64477f3e7bd41ee851d9aee53798f42842e13ffdce2d1"
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"path": "src/unlimited_ocr/adapter_registry.json",
|
| 214 |
+
"size": 880,
|
| 215 |
+
"sha256": "b06c2778707788f75a5f5c12097246c06126a6adca4bfbd8e4ba40fc6690ad6b"
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"path": "quantization/layer_sensitivity.py",
|
| 219 |
+
"size": 14664,
|
| 220 |
+
"sha256": "06b954efcfa58eb00a02e3db77b46857a13c5fcab5b1422350cda062c577f7f3"
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"path": "quantization/calibrate_precision.py",
|
| 224 |
+
"size": 10666,
|
| 225 |
+
"sha256": "0edaa2d14f435288fff96e876ea5eab8111232b34a802708bf354ee7a259c3c2"
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"path": "quantization/mixed_precision_convert.py",
|
| 229 |
+
"size": 13954,
|
| 230 |
+
"sha256": "33b01c48b0f945871633c91e46e063549062730ae7792d2540a8a0a59dd193fe"
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"path": "quantization/precision_map.json",
|
| 234 |
+
"size": 1743,
|
| 235 |
+
"sha256": "d7965210b5cab6afbde056cc91a33a41b651a254a9fa8db0b14dcdf85f7cbcbf"
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"path": "quantization/release_gate.py",
|
| 239 |
+
"size": 83309,
|
| 240 |
+
"sha256": "87e9e2c4f3e94345009f698ec6222a4b78172693ac4c9444571cf34e100d5498"
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"path": "quantization/run_pipeline.py",
|
| 244 |
+
"size": 23650,
|
| 245 |
+
"sha256": "35229b866f7433397d479e2795248d21b9d02b9d7e7000d01440ffd23c9162ec"
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"path": "benchmarks/evaluate_cer.py",
|
| 249 |
+
"size": 9407,
|
| 250 |
+
"sha256": "e6c5010a5886e3e5342825f2b970d792bf02e4ef79e44284de74702f2745e423"
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"path": "benchmarks/evaluate_tables.py",
|
| 254 |
+
"size": 9554,
|
| 255 |
+
"sha256": "a5ff42da5083eff0b70d7dc6e351395338198c1b860a3d6565e2352b159850a2"
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"path": "benchmarks/normalize_output.py",
|
| 259 |
+
"size": 4059,
|
| 260 |
+
"sha256": "dfceeb07c0df48f6f4ed925f990a45b5f8f69fc0dfc37f0fd377cea6073196e4"
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"path": "benchmarks/run_accuracy.py",
|
| 264 |
+
"size": 16808,
|
| 265 |
+
"sha256": "9e937e32b5c3599c07733f1046184624b2a0c40b68e307058c3fd141c383994e"
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"path": "benchmarks/run_performance.py",
|
| 269 |
+
"size": 12345,
|
| 270 |
+
"sha256": "8f87f459b1f4778afdfc20c078154d954edf521699b273f610132d0371d1c57a"
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"path": "benchmarks/rswa_validation.py",
|
| 274 |
+
"size": 21969,
|
| 275 |
+
"sha256": "0de31e66650604cfbdd5a48b85470002bbe447bca7caa7a0aa7ca48ac8512215"
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"path": "benchmarks/datasets.md",
|
| 279 |
+
"size": 3185,
|
| 280 |
+
"sha256": "47902cdf95278dc9509057fe64018251dce43e775fa7582590d39a916483d983"
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"path": "examples/single_image.py",
|
| 284 |
+
"size": 1123,
|
| 285 |
+
"sha256": "87afe046d745fe62e78eb584b35e8dffcd9579c13cf2f01b483b9d4ea9427cc5"
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"path": "examples/multi_page_pdf.py",
|
| 289 |
+
"size": 1182,
|
| 290 |
+
"sha256": "cc335ed4d4e55f1b0818813d6fbd00319a1fb4675fda8b6c5803b5fdf137ae37"
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
"path": "examples/batch_directory.py",
|
| 294 |
+
"size": 1411,
|
| 295 |
+
"sha256": "d9bc32620e48fc5bcbb0504b3a16a90de9a35d1083d56584b2fce1e2591a928d"
|
| 296 |
+
}
|
| 297 |
+
],
|
| 298 |
+
"aggregate_sha256": "68de2e9d2c503b3b2023d0b55ff0f7446d2046ac5a3285b5e9e23b8baeb6a370"
|
| 299 |
+
},
|
| 300 |
+
"calibration_dataset": {
|
| 301 |
+
"name": "calibration",
|
| 302 |
+
"num_samples": 10,
|
| 303 |
+
"num_digit_samples": 10,
|
| 304 |
"num_cjk_samples": 3,
|
| 305 |
"num_table_samples": 3,
|
| 306 |
"category_counts": {
|
| 307 |
"cjk_document": 3,
|
| 308 |
+
"english_document": 2,
|
| 309 |
"low_quality_scan": 2,
|
| 310 |
"table_numeric": 3
|
| 311 |
},
|
| 312 |
+
"manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
|
| 313 |
+
"content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 314 |
"files": [
|
| 315 |
{
|
| 316 |
"path": "images/0001.png",
|
|
|
|
| 334 |
},
|
| 335 |
{
|
| 336 |
"path": "images/0003.png",
|
| 337 |
+
"size": 40039,
|
| 338 |
+
"sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
"path": "ground_truth/0003.txt",
|
| 342 |
+
"size": 459,
|
| 343 |
+
"sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"path": "images/0004.png",
|
| 347 |
+
"size": 40356,
|
| 348 |
+
"sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
|
| 349 |
+
},
|
| 350 |
+
{
|
| 351 |
+
"path": "ground_truth/0004.txt",
|
| 352 |
+
"size": 459,
|
| 353 |
+
"sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
|
| 354 |
+
},
|
| 355 |
+
{
|
| 356 |
+
"path": "images/0005.png",
|
| 357 |
+
"size": 40650,
|
| 358 |
+
"sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
|
| 359 |
+
},
|
| 360 |
+
{
|
| 361 |
+
"path": "ground_truth/0005.txt",
|
| 362 |
+
"size": 459,
|
| 363 |
+
"sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"path": "images/0006.png",
|
| 367 |
+
"size": 46036,
|
| 368 |
+
"sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"path": "ground_truth/0006.txt",
|
| 372 |
+
"size": 191,
|
| 373 |
+
"sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
"path": "images/0007.png",
|
| 377 |
+
"size": 41828,
|
| 378 |
+
"sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
|
| 379 |
+
},
|
| 380 |
+
{
|
| 381 |
+
"path": "ground_truth/0007.txt",
|
| 382 |
+
"size": 195,
|
| 383 |
+
"sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
|
| 384 |
+
},
|
| 385 |
+
{
|
| 386 |
+
"path": "images/0008.png",
|
| 387 |
+
"size": 20975,
|
| 388 |
+
"sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"path": "ground_truth/0008.txt",
|
| 392 |
+
"size": 201,
|
| 393 |
+
"sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
|
| 394 |
+
},
|
| 395 |
+
{
|
| 396 |
+
"path": "images/0009.png",
|
| 397 |
+
"size": 487313,
|
| 398 |
+
"sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"path": "ground_truth/0009.txt",
|
| 402 |
+
"size": 121,
|
| 403 |
+
"sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
|
| 404 |
+
},
|
| 405 |
+
{
|
| 406 |
+
"path": "images/0010.png",
|
| 407 |
+
"size": 515820,
|
| 408 |
+
"sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
|
| 409 |
+
},
|
| 410 |
+
{
|
| 411 |
+
"path": "ground_truth/0010.txt",
|
| 412 |
+
"size": 121,
|
| 413 |
+
"sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
|
| 414 |
+
}
|
| 415 |
+
]
|
| 416 |
+
},
|
| 417 |
+
"evaluation_dataset": {
|
| 418 |
+
"name": "evaluation",
|
| 419 |
+
"num_samples": 12,
|
| 420 |
+
"num_digit_samples": 12,
|
| 421 |
+
"num_cjk_samples": 3,
|
| 422 |
+
"num_table_samples": 3,
|
| 423 |
+
"category_counts": {
|
| 424 |
+
"cjk_document": 3,
|
| 425 |
+
"english_document": 4,
|
| 426 |
+
"low_quality_scan": 2,
|
| 427 |
+
"table_numeric": 3
|
| 428 |
+
},
|
| 429 |
+
"manifest_sha256": "111646a6f59ed32689fbc658c1cb4ed60629e1bbd909d6137df469e7b489f121",
|
| 430 |
+
"content_sha256": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
|
| 431 |
+
"files": [
|
| 432 |
+
{
|
| 433 |
+
"path": "images/0001.png",
|
| 434 |
"size": 181040,
|
| 435 |
"sha256": "40330138b25a7ce9bf142a672ad0a6253517bf076af66591c4431490eff1ecfc"
|
| 436 |
},
|
| 437 |
{
|
| 438 |
+
"path": "ground_truth/0001.txt",
|
| 439 |
"size": 1333,
|
| 440 |
"sha256": "517ab5af1c090227d4dc2473ac422ecd9301e2e21ba0cc1c54d08476b1903d7e"
|
| 441 |
},
|
| 442 |
{
|
| 443 |
+
"path": "images/0002.png",
|
| 444 |
"size": 220208,
|
| 445 |
"sha256": "4316ba5ebe86c62331361e6d23329adede5e9eecf50a70db16b5439d95f5e23e"
|
| 446 |
},
|
| 447 |
{
|
| 448 |
+
"path": "ground_truth/0002.txt",
|
| 449 |
"size": 1330,
|
| 450 |
"sha256": "df3b4afaee0f3ffe0984c2f1d1201c1762bc70f4ffbc1bfe9b1242e17d794148"
|
| 451 |
},
|
| 452 |
+
{
|
| 453 |
+
"path": "images/0003.png",
|
| 454 |
+
"size": 119613,
|
| 455 |
+
"sha256": "bcf22c94459b970f1ee11088d8a902dbc56975e487c489b4714dedf8f3a4dca1"
|
| 456 |
+
},
|
| 457 |
+
{
|
| 458 |
+
"path": "ground_truth/0003.txt",
|
| 459 |
+
"size": 637,
|
| 460 |
+
"sha256": "a035ed288c8cc0e73e6cddc08c804be9b1ed32eee98e7f85c0e027bd88221b66"
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"path": "images/0004.png",
|
| 464 |
+
"size": 67701,
|
| 465 |
+
"sha256": "baea8365beb81cc9939e59d3c8a0b88ab17999122eb1729e33214450a4fba987"
|
| 466 |
+
},
|
| 467 |
+
{
|
| 468 |
+
"path": "ground_truth/0004.txt",
|
| 469 |
+
"size": 478,
|
| 470 |
+
"sha256": "af1c29967e55595362ab41e25000e00167474cbe38ea43b8816f1f1395ef6e72"
|
| 471 |
+
},
|
| 472 |
{
|
| 473 |
"path": "images/0005.png",
|
| 474 |
+
"size": 41300,
|
| 475 |
+
"sha256": "65662634fc3b5df7d006f40d276fb2869809a86a2027f464c1d6776094e16f16"
|
| 476 |
},
|
| 477 |
{
|
| 478 |
"path": "ground_truth/0005.txt",
|
| 479 |
+
"size": 459,
|
| 480 |
+
"sha256": "1d565459641aca5056fdcccabe2cdc6834107bef219645a54cd28db2e06b05ea"
|
| 481 |
},
|
| 482 |
{
|
| 483 |
"path": "images/0006.png",
|
| 484 |
+
"size": 40574,
|
| 485 |
+
"sha256": "67dbd4d07cb6e3f971937dc93a35ac81775580ee4d8895373afe6ae7e6b328dd"
|
| 486 |
},
|
| 487 |
{
|
| 488 |
"path": "ground_truth/0006.txt",
|
| 489 |
+
"size": 459,
|
| 490 |
+
"sha256": "0e134815b0bee585e3841d5a22a296c136fb43f00a9e3d48d98d2df24e884b08"
|
| 491 |
},
|
| 492 |
{
|
| 493 |
"path": "images/0007.png",
|
| 494 |
+
"size": 41027,
|
| 495 |
+
"sha256": "2b54b5222174bf4d41d92a6ee97aa1b2a3e57daca987b55906793df807dfe08f"
|
| 496 |
},
|
| 497 |
{
|
| 498 |
"path": "ground_truth/0007.txt",
|
| 499 |
+
"size": 459,
|
| 500 |
+
"sha256": "b63a7c471f4d36cc8bafddcbd7b4a8d92d6f92e73ab1e260505c2413d6e39146"
|
| 501 |
},
|
| 502 |
{
|
| 503 |
"path": "images/0008.png",
|
| 504 |
+
"size": 46025,
|
| 505 |
+
"sha256": "c8c8ed310cbdebf7e27adf3361e6ce5939922bd674af12205cea441a9b53399d"
|
| 506 |
},
|
| 507 |
{
|
| 508 |
"path": "ground_truth/0008.txt",
|
| 509 |
+
"size": 191,
|
| 510 |
+
"sha256": "246e79c3ef9faa52dd10c400eecf594314493c88ae5f7794a6129dd90e850107"
|
| 511 |
},
|
| 512 |
{
|
| 513 |
"path": "images/0009.png",
|
| 514 |
+
"size": 41617,
|
| 515 |
+
"sha256": "a00eaebf40f9369b6851a949a0aedc82a57cf1104a957e42880b1f3aa65606f0"
|
| 516 |
},
|
| 517 |
{
|
| 518 |
"path": "ground_truth/0009.txt",
|
| 519 |
+
"size": 195,
|
| 520 |
+
"sha256": "5653ef6df8a330496e73c6ddd71c9b2280818e13b11b6ff8842622ad53d24a5b"
|
| 521 |
},
|
| 522 |
{
|
| 523 |
"path": "images/0010.png",
|
| 524 |
+
"size": 21089,
|
| 525 |
+
"sha256": "59b6edec6b2ef0369f1d7fbe9fe96648fda2982db0eae377199d5edf26855900"
|
| 526 |
},
|
| 527 |
{
|
| 528 |
"path": "ground_truth/0010.txt",
|
| 529 |
+
"size": 201,
|
| 530 |
+
"sha256": "6d2f6474c94dea173288aebde046b6db40534548e8187cb776dc494abaa04ce2"
|
| 531 |
},
|
| 532 |
{
|
| 533 |
"path": "images/0011.png",
|
| 534 |
+
"size": 487332,
|
| 535 |
+
"sha256": "70f57ebf6f41479e443e4356422e6dd619ffdcf2f0df7837943f5427545cd266"
|
| 536 |
},
|
| 537 |
{
|
| 538 |
"path": "ground_truth/0011.txt",
|
| 539 |
+
"size": 121,
|
| 540 |
+
"sha256": "f550334b0b7d3508ef898d0e144593c6d005dd32d1bbcb837aeb650da56e9eb8"
|
| 541 |
},
|
| 542 |
{
|
| 543 |
"path": "images/0012.png",
|
| 544 |
+
"size": 516088,
|
| 545 |
+
"sha256": "fdd426f69928ba7bcc6008655fa0dbd950b6844eba8322491e7a5399bf052d59"
|
| 546 |
},
|
| 547 |
{
|
| 548 |
"path": "ground_truth/0012.txt",
|
| 549 |
+
"size": 121,
|
| 550 |
+
"sha256": "d040adc39553bc337df562486b3b5148727660214ec79a3af48b9cf5a2bc23f7"
|
| 551 |
}
|
| 552 |
]
|
| 553 |
},
|
| 554 |
"thresholds": {
|
| 555 |
+
"max_cer_delta_vs_bf16": 0.015,
|
| 556 |
"max_cer_delta_vs_reference": 0.005,
|
| 557 |
"max_digit_cer_delta_vs_bf16": 0.01,
|
| 558 |
"max_table_score_degradation_vs_bf16": 0.01,
|
| 559 |
"min_tps_ratio_vs_reference": 0.9,
|
| 560 |
+
"max_weight_size_gb": 4.5,
|
| 561 |
+
"min_rswa_tokens": 8192,
|
| 562 |
+
"max_rswa_repetition_rate": 0.25
|
| 563 |
},
|
| 564 |
"metrics": {
|
| 565 |
"bf16_accuracy": {
|
| 566 |
"model_path": "Unlimited-OCR",
|
| 567 |
"num_samples": 12,
|
| 568 |
+
"mean_cer": 0.205212265458972,
|
| 569 |
+
"mean_digit_cer": 0.11330969496189873,
|
| 570 |
"mean_cjk_cer": 0.3333333333333333,
|
| 571 |
"mean_table_score": 1.0,
|
| 572 |
"max_tokens": 1024,
|
|
|
|
| 575 |
"reference_accuracy": {
|
| 576 |
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 577 |
"num_samples": 12,
|
| 578 |
+
"mean_cer": 0.9073812810686629,
|
| 579 |
+
"mean_digit_cer": 0.4065110611974061,
|
| 580 |
"mean_cjk_cer": 1.0,
|
| 581 |
"mean_table_score": 1.0,
|
| 582 |
"max_tokens": 1024,
|
|
|
|
| 585 |
"candidate_accuracy": {
|
| 586 |
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 587 |
"num_samples": 12,
|
| 588 |
+
"mean_cer": 0.21633091868789586,
|
| 589 |
+
"mean_digit_cer": 0.11193906322597019,
|
| 590 |
+
"mean_cjk_cer": 0.3412698412698412,
|
| 591 |
"mean_table_score": 1.0,
|
| 592 |
"max_tokens": 1024,
|
| 593 |
"profile": "accurate"
|
| 594 |
},
|
| 595 |
"reference_performance": {
|
| 596 |
"model_path": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 597 |
+
"mean_tps": 35.07146198590831,
|
| 598 |
"mean_peak_memory_mb": 5165.991419999999,
|
| 599 |
"max_tokens": 256
|
| 600 |
},
|
| 601 |
"candidate_performance": {
|
| 602 |
"model_path": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 603 |
+
"mean_tps": 35.14004923388335,
|
| 604 |
+
"mean_peak_memory_mb": 5184.062959,
|
| 605 |
"max_tokens": 256
|
| 606 |
}
|
| 607 |
},
|
|
|
|
| 610 |
"image_path": "test_invoice.png",
|
| 611 |
"prompt": "<image>document parsing.",
|
| 612 |
"force_min_tokens": 8192,
|
| 613 |
+
"generation_settings": {
|
| 614 |
+
"temperature": 0.0,
|
| 615 |
+
"no_repeat_ngram_size": 35,
|
| 616 |
+
"ngram_window": 128
|
| 617 |
+
},
|
| 618 |
"forced_eos_token_ids": [
|
| 619 |
1
|
| 620 |
],
|
| 621 |
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 622 |
"processor": "arm",
|
| 623 |
+
"baseline_memory_mb": 3846.700552,
|
| 624 |
"test_results": [
|
| 625 |
{
|
| 626 |
"max_tokens": 512,
|
| 627 |
"tokens_generated": 512,
|
| 628 |
"tokens_generated_source": "mlx-vlm token count",
|
| 629 |
"tokens_generated_reliable": true,
|
| 630 |
+
"elapsed_seconds": 17.311166541010607,
|
| 631 |
+
"mean_tps": 32.76836342370835,
|
| 632 |
"tps_source": "mlx-vlm generation_tps",
|
| 633 |
+
"peak_memory_mb": 5183.890113,
|
| 634 |
+
"memory_growth_mb": 1337.1895610000006,
|
| 635 |
"repetition_rate": 0.0,
|
| 636 |
"completed_naturally": false,
|
| 637 |
"finish_reason": "length",
|
|
|
|
| 643 |
"tokens_generated": 2048,
|
| 644 |
"tokens_generated_source": "mlx-vlm token count",
|
| 645 |
"tokens_generated_reliable": true,
|
| 646 |
+
"elapsed_seconds": 65.73962916599703,
|
| 647 |
+
"mean_tps": 32.0896585893278,
|
| 648 |
"tps_source": "mlx-vlm generation_tps",
|
| 649 |
+
"peak_memory_mb": 5184.152257,
|
| 650 |
+
"memory_growth_mb": 1337.1895489999997,
|
| 651 |
+
"repetition_rate": 0.20496894409937888,
|
| 652 |
"completed_naturally": false,
|
| 653 |
"finish_reason": "length",
|
| 654 |
"forced_minimum_tokens": 2048,
|
|
|
|
| 659 |
"tokens_generated": 8192,
|
| 660 |
"tokens_generated_source": "mlx-vlm token count",
|
| 661 |
"tokens_generated_reliable": true,
|
| 662 |
+
"elapsed_seconds": 268.4810879580036,
|
| 663 |
+
"mean_tps": 30.743604031277776,
|
| 664 |
"tps_source": "mlx-vlm generation_tps",
|
| 665 |
+
"peak_memory_mb": 5184.152257,
|
| 666 |
+
"memory_growth_mb": 1337.1895489999997,
|
| 667 |
+
"repetition_rate": 0.20496894409937888,
|
| 668 |
"completed_naturally": false,
|
| 669 |
"finish_reason": "length",
|
| 670 |
"forced_minimum_tokens": 8192,
|
|
|
|
| 676 |
"cache_analysis_reason": "Peak memory stayed within 64 MB across a >=4x token span",
|
| 677 |
"tps_stable_over_length": true,
|
| 678 |
"max_successful_tokens": 8192,
|
| 679 |
+
"max_repetition_rate": 0.20496894409937888
|
| 680 |
},
|
| 681 |
"pass_conditions": {
|
| 682 |
"cache_bounded": true,
|
|
|
|
| 687 |
},
|
| 688 |
"sensitivity": {
|
| 689 |
"baseline": {
|
| 690 |
+
"num_samples": 10,
|
| 691 |
+
"num_digit_samples": 10,
|
| 692 |
"num_table_samples": 3,
|
| 693 |
+
"mean_cer": 0.20230911688823577,
|
| 694 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 695 |
"mean_table_score": 1.0,
|
| 696 |
+
"total_inference_time": 126.64344700002403,
|
| 697 |
+
"avg_time_per_image": 12.664344700002403
|
| 698 |
},
|
| 699 |
"groups": {
|
| 700 |
"vision_encoder": {
|
|
|
|
| 848 |
"vision_model.transformer.layers.9.self_attn.qkv_proj"
|
| 849 |
],
|
| 850 |
"metrics": {
|
| 851 |
+
"num_samples": 10,
|
| 852 |
+
"num_digit_samples": 10,
|
| 853 |
"num_table_samples": 3,
|
| 854 |
+
"mean_cer": 2.4745422809406823,
|
| 855 |
+
"mean_digit_cer": 1.7688242659575963,
|
| 856 |
"mean_table_score": 0.0,
|
| 857 |
+
"total_inference_time": 310.5234005409875,
|
| 858 |
+
"avg_time_per_image": 31.05234005409875
|
| 859 |
},
|
| 860 |
+
"cer_delta": 2.2722331640524467,
|
| 861 |
+
"digit_cer_delta": 1.6964543149172047,
|
| 862 |
"table_score_degradation": 1.0,
|
| 863 |
"sensitivity_rank": 1
|
| 864 |
},
|
|
|
|
| 869 |
"projector.layers"
|
| 870 |
],
|
| 871 |
"metrics": {
|
| 872 |
+
"num_samples": 10,
|
| 873 |
+
"num_digit_samples": 10,
|
| 874 |
"num_table_samples": 3,
|
| 875 |
+
"mean_cer": 0.20560582018493906,
|
| 876 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 877 |
"mean_table_score": 1.0,
|
| 878 |
+
"total_inference_time": 125.59270616796857,
|
| 879 |
+
"avg_time_per_image": 12.559270616796857
|
| 880 |
},
|
| 881 |
+
"cer_delta": 0.003296703296703285,
|
| 882 |
+
"digit_cer_delta": 0.0,
|
| 883 |
"table_score_degradation": 0.0,
|
| 884 |
+
"sensitivity_rank": 3
|
| 885 |
},
|
| 886 |
"token_embeddings": {
|
| 887 |
"status": "success",
|
|
|
|
| 890 |
"language_model.model.embed_tokens"
|
| 891 |
],
|
| 892 |
"metrics": {
|
| 893 |
+
"num_samples": 10,
|
| 894 |
+
"num_digit_samples": 10,
|
| 895 |
"num_table_samples": 3,
|
| 896 |
+
"mean_cer": 0.19852784584239075,
|
| 897 |
+
"mean_digit_cer": 0.07026468788249693,
|
| 898 |
"mean_table_score": 1.0,
|
| 899 |
+
"total_inference_time": 125.69877116801217,
|
| 900 |
+
"avg_time_per_image": 12.569877116801218
|
| 901 |
},
|
| 902 |
+
"cer_delta": -0.003781271045845025,
|
| 903 |
+
"digit_cer_delta": -0.002105263157894749,
|
| 904 |
"table_score_degradation": 0.0,
|
| 905 |
"sensitivity_rank": 10
|
| 906 |
},
|
|
|
|
| 922 |
"language_model.model.layers.9.self_attn.q_proj"
|
| 923 |
],
|
| 924 |
"metrics": {
|
| 925 |
+
"num_samples": 10,
|
| 926 |
+
"num_digit_samples": 10,
|
| 927 |
"num_table_samples": 3,
|
| 928 |
+
"mean_cer": 0.1810308435920253,
|
| 929 |
+
"mean_digit_cer": 0.08710679314565484,
|
| 930 |
"mean_table_score": 1.0,
|
| 931 |
+
"total_inference_time": 126.60937520701555,
|
| 932 |
+
"avg_time_per_image": 12.660937520701555
|
| 933 |
},
|
| 934 |
+
"cer_delta": -0.021278273296210487,
|
| 935 |
+
"digit_cer_delta": 0.01473684210526316,
|
| 936 |
"table_score_degradation": 0.0,
|
| 937 |
"sensitivity_rank": 11
|
| 938 |
},
|
|
|
|
| 954 |
"language_model.model.layers.9.self_attn.k_proj"
|
| 955 |
],
|
| 956 |
"metrics": {
|
| 957 |
+
"num_samples": 10,
|
| 958 |
+
"num_digit_samples": 10,
|
| 959 |
"num_table_samples": 3,
|
| 960 |
+
"mean_cer": 0.1995659706473317,
|
| 961 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 962 |
"mean_table_score": 1.0,
|
| 963 |
+
"total_inference_time": 125.29329520699685,
|
| 964 |
+
"avg_time_per_image": 12.529329520699685
|
| 965 |
},
|
| 966 |
+
"cer_delta": -0.0027431462409040763,
|
| 967 |
+
"digit_cer_delta": 0.0,
|
| 968 |
"table_score_degradation": 0.0,
|
| 969 |
"sensitivity_rank": 9
|
| 970 |
},
|
|
|
|
| 986 |
"language_model.model.layers.9.self_attn.v_proj"
|
| 987 |
],
|
| 988 |
"metrics": {
|
| 989 |
+
"num_samples": 10,
|
| 990 |
+
"num_digit_samples": 10,
|
| 991 |
"num_table_samples": 3,
|
| 992 |
+
"mean_cer": 0.20121021578933468,
|
| 993 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 994 |
"mean_table_score": 1.0,
|
| 995 |
+
"total_inference_time": 126.87845074798679,
|
| 996 |
+
"avg_time_per_image": 12.687845074798679
|
| 997 |
},
|
| 998 |
+
"cer_delta": -0.001098901098901095,
|
| 999 |
+
"digit_cer_delta": 0.0,
|
| 1000 |
"table_score_degradation": 0.0,
|
| 1001 |
+
"sensitivity_rank": 6
|
| 1002 |
},
|
| 1003 |
"attention_o_proj": {
|
| 1004 |
"status": "success",
|
|
|
|
| 1018 |
"language_model.model.layers.9.self_attn.o_proj"
|
| 1019 |
],
|
| 1020 |
"metrics": {
|
| 1021 |
+
"num_samples": 10,
|
| 1022 |
+
"num_digit_samples": 10,
|
| 1023 |
"num_table_samples": 3,
|
| 1024 |
+
"mean_cer": 0.2058682899333709,
|
| 1025 |
+
"mean_digit_cer": 0.06383873929008568,
|
| 1026 |
"mean_table_score": 1.0,
|
| 1027 |
+
"total_inference_time": 128.61252554102975,
|
| 1028 |
+
"avg_time_per_image": 12.861252554102975
|
| 1029 |
},
|
| 1030 |
+
"cer_delta": 0.0035591730451351167,
|
| 1031 |
+
"digit_cer_delta": -0.008531211750305995,
|
| 1032 |
"table_score_degradation": 0.0,
|
| 1033 |
+
"sensitivity_rank": 2
|
| 1034 |
},
|
| 1035 |
"dense_mlp": {
|
| 1036 |
"status": "success",
|
|
|
|
| 1041 |
"language_model.model.layers.0.mlp.up_proj"
|
| 1042 |
],
|
| 1043 |
"metrics": {
|
| 1044 |
+
"num_samples": 10,
|
| 1045 |
+
"num_digit_samples": 10,
|
| 1046 |
"num_table_samples": 3,
|
| 1047 |
+
"mean_cer": 0.20121021578933468,
|
| 1048 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 1049 |
"mean_table_score": 1.0,
|
| 1050 |
+
"total_inference_time": 127.19930095798918,
|
| 1051 |
+
"avg_time_per_image": 12.719930095798919
|
| 1052 |
},
|
| 1053 |
+
"cer_delta": -0.001098901098901095,
|
| 1054 |
+
"digit_cer_delta": 0.0,
|
| 1055 |
"table_score_degradation": 0.0,
|
| 1056 |
+
"sensitivity_rank": 7
|
| 1057 |
},
|
| 1058 |
"shared_experts": {
|
| 1059 |
"status": "success",
|
|
|
|
| 1094 |
"language_model.model.layers.9.mlp.shared_experts.up_proj"
|
| 1095 |
],
|
| 1096 |
"metrics": {
|
| 1097 |
+
"num_samples": 10,
|
| 1098 |
+
"num_digit_samples": 10,
|
| 1099 |
"num_table_samples": 3,
|
| 1100 |
+
"mean_cer": 0.20009091014419536,
|
| 1101 |
+
"mean_digit_cer": 0.08267594859241126,
|
| 1102 |
"mean_table_score": 1.0,
|
| 1103 |
+
"total_inference_time": 128.06347554198874,
|
| 1104 |
+
"avg_time_per_image": 12.806347554198874
|
| 1105 |
},
|
| 1106 |
+
"cer_delta": -0.002218206744040413,
|
| 1107 |
+
"digit_cer_delta": 0.01030599755201958,
|
| 1108 |
"table_score_degradation": 0.0,
|
| 1109 |
+
"sensitivity_rank": 8
|
| 1110 |
},
|
| 1111 |
"routed_experts": {
|
| 1112 |
"status": "success",
|
|
|
|
| 1147 |
"language_model.model.layers.9.mlp.switch_mlp.up_proj"
|
| 1148 |
],
|
| 1149 |
"metrics": {
|
| 1150 |
+
"num_samples": 10,
|
| 1151 |
+
"num_digit_samples": 10,
|
| 1152 |
"num_table_samples": 3,
|
| 1153 |
+
"mean_cer": 0.2033268371469385,
|
| 1154 |
+
"mean_digit_cer": 0.06899173806609546,
|
| 1155 |
"mean_table_score": 1.0,
|
| 1156 |
+
"total_inference_time": 125.24760646000504,
|
| 1157 |
+
"avg_time_per_image": 12.524760646000505
|
| 1158 |
},
|
| 1159 |
+
"cer_delta": 0.0010177202587027256,
|
| 1160 |
+
"digit_cer_delta": -0.0033782129742962186,
|
| 1161 |
"table_score_degradation": 0.0,
|
| 1162 |
+
"sensitivity_rank": 5
|
| 1163 |
},
|
| 1164 |
"lm_head": {
|
| 1165 |
"status": "success",
|
|
|
|
| 1168 |
"language_model.lm_head"
|
| 1169 |
],
|
| 1170 |
"metrics": {
|
| 1171 |
+
"num_samples": 10,
|
| 1172 |
+
"num_digit_samples": 10,
|
| 1173 |
"num_table_samples": 3,
|
| 1174 |
+
"mean_cer": 0.20560582018493906,
|
| 1175 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 1176 |
"mean_table_score": 1.0,
|
| 1177 |
+
"total_inference_time": 122.19146683299914,
|
| 1178 |
+
"avg_time_per_image": 12.219146683299915
|
| 1179 |
},
|
| 1180 |
+
"cer_delta": 0.003296703296703285,
|
| 1181 |
+
"digit_cer_delta": 0.0,
|
| 1182 |
"table_score_degradation": 0.0,
|
| 1183 |
+
"sensitivity_rank": 4
|
| 1184 |
}
|
| 1185 |
}
|
| 1186 |
},
|
| 1187 |
"calibration": {
|
| 1188 |
+
"schema_version": 3,
|
| 1189 |
+
"created_at": "2026-07-21T05:26:12.430585+00:00",
|
| 1190 |
"target_pattern": "language_model.lm_head",
|
| 1191 |
"selection_policy": "fastest candidate passing existing quality and throughput limits",
|
| 1192 |
"thresholds": {
|
| 1193 |
+
"max_cer_delta_vs_bf16": 0.015,
|
| 1194 |
"max_cer_delta_vs_reference": 0.005,
|
| 1195 |
"max_digit_cer_delta_vs_bf16": 0.01,
|
| 1196 |
"max_table_score_degradation_vs_bf16": 0.01,
|
| 1197 |
"min_tps_ratio_vs_reference": 0.9,
|
| 1198 |
+
"max_weight_size_gb": 4.5,
|
| 1199 |
+
"min_rswa_tokens": 8192,
|
| 1200 |
+
"max_rswa_repetition_rate": 0.25
|
| 1201 |
},
|
| 1202 |
"experiments": [
|
| 1203 |
{
|
| 1204 |
"label": "bf16-head",
|
| 1205 |
"precision": "bfloat16",
|
| 1206 |
+
"passed": true,
|
| 1207 |
"checks": {
|
| 1208 |
"cer": true,
|
| 1209 |
"digit_cer": true,
|
| 1210 |
"table_score": true,
|
| 1211 |
+
"throughput": true
|
| 1212 |
},
|
| 1213 |
"metrics": {
|
| 1214 |
+
"mean_cer": 0.19010066585760363,
|
| 1215 |
+
"mean_digit_cer": 0.06278610771113831,
|
| 1216 |
"mean_table_score": 1.0,
|
| 1217 |
+
"mean_tps": 38.770309481806514
|
| 1218 |
},
|
| 1219 |
"deltas": {
|
| 1220 |
+
"cer_vs_bf16": -0.012208451030632145,
|
| 1221 |
+
"digit_cer_vs_bf16": -0.00958384332925337,
|
| 1222 |
"table_degradation_vs_bf16": 0.0,
|
| 1223 |
+
"tps_ratio_vs_reference": 0.9595978674779929
|
| 1224 |
}
|
| 1225 |
},
|
| 1226 |
{
|
|
|
|
| 1228 |
"precision": "mxfp8",
|
| 1229 |
"passed": false,
|
| 1230 |
"checks": {
|
| 1231 |
+
"cer": true,
|
| 1232 |
+
"digit_cer": false,
|
| 1233 |
"table_score": true,
|
| 1234 |
"throughput": true
|
| 1235 |
},
|
| 1236 |
"metrics": {
|
| 1237 |
+
"mean_cer": 0.21607021200652196,
|
| 1238 |
+
"mean_digit_cer": 0.1052422480620155,
|
| 1239 |
"mean_table_score": 1.0,
|
| 1240 |
+
"mean_tps": 41.326731942543184
|
| 1241 |
},
|
| 1242 |
"deltas": {
|
| 1243 |
+
"cer_vs_bf16": 0.013761095118286187,
|
| 1244 |
+
"digit_cer_vs_bf16": 0.03287229702162382,
|
| 1245 |
"table_degradation_vs_bf16": 0.0,
|
| 1246 |
+
"tps_ratio_vs_reference": 1.0228714800563738
|
| 1247 |
}
|
| 1248 |
},
|
| 1249 |
{
|
|
|
|
| 1257 |
"throughput": true
|
| 1258 |
},
|
| 1259 |
"metrics": {
|
| 1260 |
+
"mean_cer": 0.19105830313811087,
|
| 1261 |
+
"mean_digit_cer": 0.07857558139534884,
|
| 1262 |
"mean_table_score": 1.0,
|
| 1263 |
+
"mean_tps": 39.58614991640946
|
| 1264 |
},
|
| 1265 |
"deltas": {
|
| 1266 |
+
"cer_vs_bf16": -0.011250813750124905,
|
| 1267 |
+
"digit_cer_vs_bf16": 0.0062056303549571645,
|
| 1268 |
"table_degradation_vs_bf16": 0.0,
|
| 1269 |
+
"tps_ratio_vs_reference": 0.979790606502031
|
| 1270 |
}
|
| 1271 |
}
|
| 1272 |
],
|
|
|
|
| 1276 |
},
|
| 1277 |
"precision_overrides": {
|
| 1278 |
"language_model.lm_head": "affine8"
|
| 1279 |
+
},
|
| 1280 |
+
"input_artifacts": {
|
| 1281 |
+
"calibration_baseline_accuracy": {
|
| 1282 |
+
"filename": "calibration_baseline_accuracy.json",
|
| 1283 |
+
"size": 5680,
|
| 1284 |
+
"sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
|
| 1285 |
+
},
|
| 1286 |
+
"calibration_reference_performance": {
|
| 1287 |
+
"filename": "calibration_reference_performance.json",
|
| 1288 |
+
"size": 2271,
|
| 1289 |
+
"sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
|
| 1290 |
+
},
|
| 1291 |
+
"calibration_bfloat16_accuracy": {
|
| 1292 |
+
"filename": "calibration_bfloat16_accuracy.json",
|
| 1293 |
+
"size": 5678,
|
| 1294 |
+
"sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
|
| 1295 |
+
},
|
| 1296 |
+
"calibration_bfloat16_performance": {
|
| 1297 |
+
"filename": "calibration_bfloat16_performance.json",
|
| 1298 |
+
"size": 2244,
|
| 1299 |
+
"sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
|
| 1300 |
+
},
|
| 1301 |
+
"calibration_mxfp8_accuracy": {
|
| 1302 |
+
"filename": "calibration_mxfp8_accuracy.json",
|
| 1303 |
+
"size": 5689,
|
| 1304 |
+
"sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
|
| 1305 |
+
},
|
| 1306 |
+
"calibration_mxfp8_performance": {
|
| 1307 |
+
"filename": "calibration_mxfp8_performance.json",
|
| 1308 |
+
"size": 2252,
|
| 1309 |
+
"sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
|
| 1310 |
+
},
|
| 1311 |
+
"calibration_affine8_accuracy": {
|
| 1312 |
+
"filename": "calibration_affine8_accuracy.json",
|
| 1313 |
+
"size": 5657,
|
| 1314 |
+
"sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
|
| 1315 |
+
},
|
| 1316 |
+
"calibration_affine8_performance": {
|
| 1317 |
+
"filename": "calibration_affine8_performance.json",
|
| 1318 |
+
"size": 2247,
|
| 1319 |
+
"sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
|
| 1320 |
+
}
|
| 1321 |
+
},
|
| 1322 |
+
"dataset": {
|
| 1323 |
+
"name": "calibration",
|
| 1324 |
+
"num_samples": 10,
|
| 1325 |
+
"num_digit_samples": 10,
|
| 1326 |
+
"num_cjk_samples": 3,
|
| 1327 |
+
"num_table_samples": 3,
|
| 1328 |
+
"category_counts": {
|
| 1329 |
+
"cjk_document": 3,
|
| 1330 |
+
"english_document": 2,
|
| 1331 |
+
"low_quality_scan": 2,
|
| 1332 |
+
"table_numeric": 3
|
| 1333 |
+
},
|
| 1334 |
+
"manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
|
| 1335 |
+
"content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 1336 |
+
"files": [
|
| 1337 |
+
{
|
| 1338 |
+
"path": "images/0001.png",
|
| 1339 |
+
"size": 243645,
|
| 1340 |
+
"sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
|
| 1341 |
+
},
|
| 1342 |
+
{
|
| 1343 |
+
"path": "ground_truth/0001.txt",
|
| 1344 |
+
"size": 1346,
|
| 1345 |
+
"sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
|
| 1346 |
+
},
|
| 1347 |
+
{
|
| 1348 |
+
"path": "images/0002.png",
|
| 1349 |
+
"size": 208699,
|
| 1350 |
+
"sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
|
| 1351 |
+
},
|
| 1352 |
+
{
|
| 1353 |
+
"path": "ground_truth/0002.txt",
|
| 1354 |
+
"size": 838,
|
| 1355 |
+
"sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
|
| 1356 |
+
},
|
| 1357 |
+
{
|
| 1358 |
+
"path": "images/0003.png",
|
| 1359 |
+
"size": 40039,
|
| 1360 |
+
"sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
|
| 1361 |
+
},
|
| 1362 |
+
{
|
| 1363 |
+
"path": "ground_truth/0003.txt",
|
| 1364 |
+
"size": 459,
|
| 1365 |
+
"sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
|
| 1366 |
+
},
|
| 1367 |
+
{
|
| 1368 |
+
"path": "images/0004.png",
|
| 1369 |
+
"size": 40356,
|
| 1370 |
+
"sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
|
| 1371 |
+
},
|
| 1372 |
+
{
|
| 1373 |
+
"path": "ground_truth/0004.txt",
|
| 1374 |
+
"size": 459,
|
| 1375 |
+
"sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
|
| 1376 |
+
},
|
| 1377 |
+
{
|
| 1378 |
+
"path": "images/0005.png",
|
| 1379 |
+
"size": 40650,
|
| 1380 |
+
"sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
|
| 1381 |
+
},
|
| 1382 |
+
{
|
| 1383 |
+
"path": "ground_truth/0005.txt",
|
| 1384 |
+
"size": 459,
|
| 1385 |
+
"sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
|
| 1386 |
+
},
|
| 1387 |
+
{
|
| 1388 |
+
"path": "images/0006.png",
|
| 1389 |
+
"size": 46036,
|
| 1390 |
+
"sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
|
| 1391 |
+
},
|
| 1392 |
+
{
|
| 1393 |
+
"path": "ground_truth/0006.txt",
|
| 1394 |
+
"size": 191,
|
| 1395 |
+
"sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
|
| 1396 |
+
},
|
| 1397 |
+
{
|
| 1398 |
+
"path": "images/0007.png",
|
| 1399 |
+
"size": 41828,
|
| 1400 |
+
"sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
|
| 1401 |
+
},
|
| 1402 |
+
{
|
| 1403 |
+
"path": "ground_truth/0007.txt",
|
| 1404 |
+
"size": 195,
|
| 1405 |
+
"sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
|
| 1406 |
+
},
|
| 1407 |
+
{
|
| 1408 |
+
"path": "images/0008.png",
|
| 1409 |
+
"size": 20975,
|
| 1410 |
+
"sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
|
| 1411 |
+
},
|
| 1412 |
+
{
|
| 1413 |
+
"path": "ground_truth/0008.txt",
|
| 1414 |
+
"size": 201,
|
| 1415 |
+
"sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
|
| 1416 |
+
},
|
| 1417 |
+
{
|
| 1418 |
+
"path": "images/0009.png",
|
| 1419 |
+
"size": 487313,
|
| 1420 |
+
"sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
|
| 1421 |
+
},
|
| 1422 |
+
{
|
| 1423 |
+
"path": "ground_truth/0009.txt",
|
| 1424 |
+
"size": 121,
|
| 1425 |
+
"sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
|
| 1426 |
+
},
|
| 1427 |
+
{
|
| 1428 |
+
"path": "images/0010.png",
|
| 1429 |
+
"size": 515820,
|
| 1430 |
+
"sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
|
| 1431 |
+
},
|
| 1432 |
+
{
|
| 1433 |
+
"path": "ground_truth/0010.txt",
|
| 1434 |
+
"size": 121,
|
| 1435 |
+
"sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
|
| 1436 |
+
}
|
| 1437 |
+
]
|
| 1438 |
}
|
| 1439 |
},
|
| 1440 |
"precision_map": {
|
| 1441 |
"vision_model": "bfloat16",
|
| 1442 |
+
"projector": "bfloat16",
|
| 1443 |
"language_model.model.embed_tokens": "mxfp8",
|
| 1444 |
"language_model.model.norm": "bfloat16",
|
| 1445 |
"language_model.lm_head": "affine8",
|
|
|
|
| 1471 |
"vision_encoder": {
|
| 1472 |
"precision": "bfloat16",
|
| 1473 |
"reasons": [
|
| 1474 |
+
"cer_delta=2.272233",
|
| 1475 |
+
"digit_cer_delta=1.696454",
|
| 1476 |
"table_score_degradation=1.000000",
|
| 1477 |
"protected OCR-critical group"
|
| 1478 |
]
|
| 1479 |
},
|
| 1480 |
"vision_projector": {
|
| 1481 |
+
"precision": "bfloat16",
|
| 1482 |
"reasons": [
|
| 1483 |
+
"protected unless all measured quality deltas are non-degrading"
|
| 1484 |
]
|
| 1485 |
},
|
| 1486 |
"token_embeddings": {
|
|
|
|
| 1548 |
}
|
| 1549 |
},
|
| 1550 |
"artifacts": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1551 |
"reference_accuracy": {
|
| 1552 |
"filename": "reference_accuracy.json",
|
| 1553 |
+
"size": 6664,
|
| 1554 |
+
"sha256": "2cd131108b70f0252995c0c096bd203bd7f7e6d06ce8921e1585b8a0a4523ad7"
|
| 1555 |
},
|
| 1556 |
+
"generated_precision_map": {
|
| 1557 |
+
"filename": "generated_precision_map.json",
|
| 1558 |
+
"size": 3337,
|
| 1559 |
+
"sha256": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1560 |
},
|
| 1561 |
"candidate_performance": {
|
| 1562 |
"filename": "candidate_performance.json",
|
| 1563 |
+
"size": 2261,
|
| 1564 |
+
"sha256": "71034ab96428b1ff5f7d700166aa393bacca91a465bb265bfe33af96d91dde52"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1565 |
},
|
| 1566 |
"sensitivity_results": {
|
| 1567 |
"filename": "sensitivity_results.json",
|
| 1568 |
+
"size": 26432,
|
| 1569 |
+
"sha256": "170e57762b46a880360c3c6b89e2ed96e6ceb08454541a0baa7432e53f60ce49"
|
| 1570 |
},
|
| 1571 |
"calibration_results": {
|
| 1572 |
"filename": "calibration_results.json",
|
| 1573 |
+
"size": 8019,
|
| 1574 |
+
"sha256": "b077712a0521f50e0b8c6ba1ca20e66d0ee64f750d6a545a1b3ab378af50adf7"
|
| 1575 |
},
|
| 1576 |
+
"candidate_accuracy": {
|
| 1577 |
+
"filename": "candidate_accuracy.json",
|
| 1578 |
+
"size": 6562,
|
| 1579 |
+
"sha256": "60bbb1f0ac5ea87e64d57889c5ade08aa5341e9558c530c009cb7fa546e68c25"
|
| 1580 |
+
},
|
| 1581 |
+
"bf16_accuracy": {
|
| 1582 |
+
"filename": "bf16_accuracy.json",
|
| 1583 |
+
"size": 6499,
|
| 1584 |
+
"sha256": "04289ab8cd0ee4019a341eaab83617eeca3cca99f2f96f236cb8725036789659"
|
| 1585 |
+
},
|
| 1586 |
+
"reference_performance": {
|
| 1587 |
+
"filename": "reference_performance.json",
|
| 1588 |
+
"size": 2271,
|
| 1589 |
+
"sha256": "1ac6b646a090cd052eea48ff50cc96d666204e986273a98f43daf263cecb270d"
|
| 1590 |
},
|
| 1591 |
"provenance": {
|
| 1592 |
"filename": "provenance.json",
|
| 1593 |
+
"size": 9280,
|
| 1594 |
+
"sha256": "958930c6a480798922a88287ba53d75de13df2fa7710d387c7138bbf300601e8"
|
| 1595 |
+
},
|
| 1596 |
+
"candidate_rswa": {
|
| 1597 |
+
"filename": "candidate_rswa.json",
|
| 1598 |
+
"size": 2483,
|
| 1599 |
+
"sha256": "1516b35616be2947d5595a3c9779f7b960adee4ad472336f50f5de88a151ebe0"
|
| 1600 |
+
},
|
| 1601 |
+
"calibration_affine8_accuracy": {
|
| 1602 |
+
"filename": "calibration_affine8_accuracy.json",
|
| 1603 |
+
"size": 5657,
|
| 1604 |
+
"sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
|
| 1605 |
+
},
|
| 1606 |
+
"calibration_bfloat16_accuracy": {
|
| 1607 |
+
"filename": "calibration_bfloat16_accuracy.json",
|
| 1608 |
+
"size": 5678,
|
| 1609 |
+
"sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
|
| 1610 |
+
},
|
| 1611 |
+
"calibration_mxfp8_performance": {
|
| 1612 |
+
"filename": "calibration_mxfp8_performance.json",
|
| 1613 |
+
"size": 2252,
|
| 1614 |
+
"sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
|
| 1615 |
+
},
|
| 1616 |
+
"calibration_reference_performance": {
|
| 1617 |
+
"filename": "calibration_reference_performance.json",
|
| 1618 |
+
"size": 2271,
|
| 1619 |
+
"sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
|
| 1620 |
+
},
|
| 1621 |
+
"calibration_mxfp8_accuracy": {
|
| 1622 |
+
"filename": "calibration_mxfp8_accuracy.json",
|
| 1623 |
+
"size": 5689,
|
| 1624 |
+
"sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
|
| 1625 |
+
},
|
| 1626 |
+
"calibration_affine8_performance": {
|
| 1627 |
+
"filename": "calibration_affine8_performance.json",
|
| 1628 |
+
"size": 2247,
|
| 1629 |
+
"sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
|
| 1630 |
+
},
|
| 1631 |
+
"calibration_bfloat16_performance": {
|
| 1632 |
+
"filename": "calibration_bfloat16_performance.json",
|
| 1633 |
+
"size": 2244,
|
| 1634 |
+
"sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
|
| 1635 |
+
},
|
| 1636 |
+
"calibration_baseline_accuracy": {
|
| 1637 |
+
"filename": "calibration_baseline_accuracy.json",
|
| 1638 |
+
"size": 5680,
|
| 1639 |
+
"sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
|
| 1640 |
}
|
| 1641 |
},
|
| 1642 |
"gates": [
|
| 1643 |
{
|
| 1644 |
"name": "weights_are_distinct",
|
| 1645 |
"passed": true,
|
| 1646 |
+
"actual": "5ff9be1dc0a833b608f6cece5b2e257c5f152eab7de160c5f07d3e5cd9462549",
|
| 1647 |
"limit": "different from 439c6acfe5e277537dfe3368b94a145a6b0da4c39e4bd00582977d030f292ad1",
|
| 1648 |
"detail": "Candidate aggregate digest must differ from the Sahil reference"
|
| 1649 |
},
|
| 1650 |
{
|
| 1651 |
"name": "weight_size_gb",
|
| 1652 |
"passed": true,
|
| 1653 |
+
"actual": 3.5815872186794877,
|
| 1654 |
"limit": 4.5,
|
| 1655 |
"detail": "Candidate Safetensors size"
|
| 1656 |
},
|
|
|
|
| 1663 |
"mxfp8": true,
|
| 1664 |
"sliding_window": true,
|
| 1665 |
"processor_class": true,
|
| 1666 |
+
"sft_format": true,
|
| 1667 |
+
"precision_map": true,
|
| 1668 |
+
"quantization_summary": true,
|
| 1669 |
+
"protected_modules_preserved": true
|
| 1670 |
},
|
| 1671 |
"limit": true,
|
| 1672 |
"detail": "Native Unlimited-OCR, MXFP8, and R-SWA metadata"
|
| 1673 |
},
|
| 1674 |
+
{
|
| 1675 |
+
"name": "candidate_source_provenance",
|
| 1676 |
+
"passed": true,
|
| 1677 |
+
"actual": {
|
| 1678 |
+
"model": "Unlimited-OCR",
|
| 1679 |
+
"revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead"
|
| 1680 |
+
},
|
| 1681 |
+
"limit": {
|
| 1682 |
+
"model": [
|
| 1683 |
+
"baidu/Unlimited-OCR",
|
| 1684 |
+
"Unlimited-OCR"
|
| 1685 |
+
],
|
| 1686 |
+
"revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead"
|
| 1687 |
+
},
|
| 1688 |
+
"detail": "Converted metadata must bind the exact BF16 source commit"
|
| 1689 |
+
},
|
| 1690 |
{
|
| 1691 |
"name": "evaluation_coverage",
|
| 1692 |
"passed": true,
|
|
|
|
| 1700 |
"detail": "Dataset must cover ordinary text, digits, CJK, and tables"
|
| 1701 |
},
|
| 1702 |
{
|
| 1703 |
+
"name": "held_out_evaluation_dataset",
|
| 1704 |
+
"passed": true,
|
| 1705 |
+
"actual": {
|
| 1706 |
+
"passed": true,
|
| 1707 |
+
"calibration_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 1708 |
+
"evaluation_digest": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
|
| 1709 |
+
"overlapping_image_sha256": [],
|
| 1710 |
+
"overlapping_ground_truth_sha256": []
|
| 1711 |
+
},
|
| 1712 |
+
"limit": "distinct dataset digests and no shared image or ground-truth hashes",
|
| 1713 |
+
"detail": "Final quality evidence must not reuse model-selection samples"
|
| 1714 |
+
},
|
| 1715 |
+
{
|
| 1716 |
+
"name": "accuracy_aggregates_recomputed",
|
| 1717 |
"passed": true,
|
| 1718 |
"actual": [
|
| 1719 |
+
true,
|
| 1720 |
+
true,
|
| 1721 |
+
true
|
| 1722 |
+
],
|
| 1723 |
+
"limit": [
|
| 1724 |
+
true,
|
| 1725 |
+
true,
|
| 1726 |
+
true
|
| 1727 |
],
|
| 1728 |
+
"detail": "Every reported accuracy aggregate must recompute from per-file rows"
|
| 1729 |
+
},
|
| 1730 |
+
{
|
| 1731 |
+
"name": "same_evaluation_samples",
|
| 1732 |
+
"passed": true,
|
| 1733 |
+
"actual": {
|
| 1734 |
+
"counts": [
|
| 1735 |
+
12,
|
| 1736 |
+
12,
|
| 1737 |
+
12
|
| 1738 |
+
],
|
| 1739 |
+
"file_counts": [
|
| 1740 |
+
12,
|
| 1741 |
+
12,
|
| 1742 |
+
12
|
| 1743 |
+
]
|
| 1744 |
+
},
|
| 1745 |
+
"limit": {
|
| 1746 |
+
"count": 12,
|
| 1747 |
+
"files": [
|
| 1748 |
+
"0001.png",
|
| 1749 |
+
"0002.png",
|
| 1750 |
+
"0003.png",
|
| 1751 |
+
"0004.png",
|
| 1752 |
+
"0005.png",
|
| 1753 |
+
"0006.png",
|
| 1754 |
+
"0007.png",
|
| 1755 |
+
"0008.png",
|
| 1756 |
+
"0009.png",
|
| 1757 |
+
"0010.png",
|
| 1758 |
+
"0011.png",
|
| 1759 |
+
"0012.png"
|
| 1760 |
+
]
|
| 1761 |
+
},
|
| 1762 |
+
"detail": "All three checkpoints must run every identical evaluation file"
|
| 1763 |
+
},
|
| 1764 |
+
{
|
| 1765 |
+
"name": "model_identities",
|
| 1766 |
+
"passed": true,
|
| 1767 |
+
"actual": {
|
| 1768 |
+
"bf16": "Unlimited-OCR",
|
| 1769 |
+
"reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 1770 |
+
"candidate": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 1771 |
+
"reference_performance": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 1772 |
+
"candidate_performance": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 1773 |
+
"rswa": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
|
| 1774 |
+
},
|
| 1775 |
+
"limit": {
|
| 1776 |
+
"bf16": "Unlimited-OCR",
|
| 1777 |
+
"reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 1778 |
+
"candidate": "AX-Unlimited-OCR-3B-MoE-MLX-MXFP8"
|
| 1779 |
+
},
|
| 1780 |
+
"detail": "Accuracy, performance, and R-SWA evidence must identify exact models"
|
| 1781 |
+
},
|
| 1782 |
+
{
|
| 1783 |
+
"name": "immutable_model_revisions",
|
| 1784 |
+
"passed": true,
|
| 1785 |
+
"actual": {
|
| 1786 |
+
"bf16_accuracy": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 1787 |
+
"reference_accuracy": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 1788 |
+
"candidate_accuracy": null,
|
| 1789 |
+
"reference_performance": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 1790 |
+
"candidate_performance": null
|
| 1791 |
+
},
|
| 1792 |
+
"limit": {
|
| 1793 |
+
"bf16_accuracy": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 1794 |
+
"reference_accuracy": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 1795 |
+
"candidate_accuracy": null,
|
| 1796 |
+
"reference_performance": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 1797 |
+
"candidate_performance": null
|
| 1798 |
+
},
|
| 1799 |
+
"detail": "Remote source and reference evidence must name exact Hub commits"
|
| 1800 |
+
},
|
| 1801 |
+
{
|
| 1802 |
+
"name": "same_accuracy_recipe",
|
| 1803 |
+
"passed": true,
|
| 1804 |
+
"actual": {
|
| 1805 |
+
"prompt": [
|
| 1806 |
+
"<image>document parsing.",
|
| 1807 |
+
"<image>document parsing.",
|
| 1808 |
+
"<image>document parsing."
|
| 1809 |
+
],
|
| 1810 |
+
"max_tokens": [
|
| 1811 |
+
1024,
|
| 1812 |
+
1024,
|
| 1813 |
+
1024
|
| 1814 |
+
],
|
| 1815 |
+
"profile": [
|
| 1816 |
+
"accurate",
|
| 1817 |
+
"accurate",
|
| 1818 |
+
"accurate"
|
| 1819 |
+
],
|
| 1820 |
+
"generation_settings": [
|
| 1821 |
+
{
|
| 1822 |
+
"temperature": 0.0,
|
| 1823 |
+
"top_p": 1.0,
|
| 1824 |
+
"repetition_penalty": 1.0,
|
| 1825 |
+
"no_repeat_ngram_size": 35,
|
| 1826 |
+
"ngram_window": 128
|
| 1827 |
+
},
|
| 1828 |
+
{
|
| 1829 |
+
"temperature": 0.0,
|
| 1830 |
+
"top_p": 1.0,
|
| 1831 |
+
"repetition_penalty": 1.0,
|
| 1832 |
+
"no_repeat_ngram_size": 35,
|
| 1833 |
+
"ngram_window": 128
|
| 1834 |
+
},
|
| 1835 |
+
{
|
| 1836 |
+
"temperature": 0.0,
|
| 1837 |
+
"top_p": 1.0,
|
| 1838 |
+
"repetition_penalty": 1.0,
|
| 1839 |
+
"no_repeat_ngram_size": 35,
|
| 1840 |
+
"ngram_window": 128
|
| 1841 |
+
}
|
| 1842 |
+
]
|
| 1843 |
+
},
|
| 1844 |
+
"limit": "identical official MLX OCR recipe",
|
| 1845 |
+
"detail": "Accuracy runs must use the same deterministic OCR generation settings"
|
| 1846 |
},
|
| 1847 |
{
|
| 1848 |
"name": "candidate_cer_vs_bf16",
|
| 1849 |
"passed": true,
|
| 1850 |
+
"actual": 0.011118653228923858,
|
| 1851 |
+
"limit": 0.015,
|
| 1852 |
"detail": "Candidate minus BF16 absolute mean CER"
|
| 1853 |
},
|
| 1854 |
{
|
| 1855 |
"name": "candidate_cer_vs_reference",
|
| 1856 |
"passed": true,
|
| 1857 |
+
"actual": -0.691050362380767,
|
| 1858 |
"limit": 0.005,
|
| 1859 |
"detail": "Candidate minus Sahil-reference absolute mean CER"
|
| 1860 |
},
|
| 1861 |
{
|
| 1862 |
"name": "candidate_digit_cer_vs_bf16",
|
| 1863 |
"passed": true,
|
| 1864 |
+
"actual": -0.0013706317359285375,
|
| 1865 |
"limit": 0.01,
|
| 1866 |
"detail": "Candidate minus BF16 digit CER"
|
| 1867 |
},
|
|
|
|
| 1872 |
"limit": 0.01,
|
| 1873 |
"detail": "BF16 minus candidate mean table score"
|
| 1874 |
},
|
| 1875 |
+
{
|
| 1876 |
+
"name": "performance_aggregates_recomputed",
|
| 1877 |
+
"passed": true,
|
| 1878 |
+
"actual": {
|
| 1879 |
+
"reference": true,
|
| 1880 |
+
"candidate": true
|
| 1881 |
+
},
|
| 1882 |
+
"limit": {
|
| 1883 |
+
"reference": true,
|
| 1884 |
+
"candidate": true
|
| 1885 |
+
},
|
| 1886 |
+
"detail": "Performance means must recompute from complete benchmark runs"
|
| 1887 |
+
},
|
| 1888 |
+
{
|
| 1889 |
+
"name": "same_performance_setup",
|
| 1890 |
+
"passed": true,
|
| 1891 |
+
"actual": {
|
| 1892 |
+
"image_path": [
|
| 1893 |
+
"test_invoice.png",
|
| 1894 |
+
"test_invoice.png"
|
| 1895 |
+
],
|
| 1896 |
+
"prompt": [
|
| 1897 |
+
"<image>document parsing.",
|
| 1898 |
+
"<image>document parsing."
|
| 1899 |
+
],
|
| 1900 |
+
"max_tokens": [
|
| 1901 |
+
256,
|
| 1902 |
+
256
|
| 1903 |
+
],
|
| 1904 |
+
"num_warmup": [
|
| 1905 |
+
1,
|
| 1906 |
+
1
|
| 1907 |
+
],
|
| 1908 |
+
"num_runs": [
|
| 1909 |
+
3,
|
| 1910 |
+
3
|
| 1911 |
+
],
|
| 1912 |
+
"system": [
|
| 1913 |
+
{
|
| 1914 |
+
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 1915 |
+
"processor": "arm",
|
| 1916 |
+
"python_version": "3.14.6",
|
| 1917 |
+
"machine": "arm64",
|
| 1918 |
+
"mlx_version": "0.32.0",
|
| 1919 |
+
"mlx_vlm_version": "0.6.6",
|
| 1920 |
+
"chip": "Apple M3 Max",
|
| 1921 |
+
"total_memory_gb": 128.0
|
| 1922 |
+
},
|
| 1923 |
+
{
|
| 1924 |
+
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
|
| 1925 |
+
"processor": "arm",
|
| 1926 |
+
"python_version": "3.14.6",
|
| 1927 |
+
"machine": "arm64",
|
| 1928 |
+
"mlx_version": "0.32.0",
|
| 1929 |
+
"mlx_vlm_version": "0.6.6",
|
| 1930 |
+
"chip": "Apple M3 Max",
|
| 1931 |
+
"total_memory_gb": 128.0
|
| 1932 |
+
}
|
| 1933 |
+
]
|
| 1934 |
+
},
|
| 1935 |
+
"limit": "identical setup with at least three complete runs",
|
| 1936 |
+
"detail": "Performance comparisons must use the same host and benchmark recipe"
|
| 1937 |
+
},
|
| 1938 |
{
|
| 1939 |
"name": "candidate_tps_vs_reference",
|
| 1940 |
"passed": true,
|
| 1941 |
+
"actual": 1.001955642681865,
|
| 1942 |
"limit": 0.9,
|
| 1943 |
"detail": "Candidate decode throughput divided by Sahil-reference throughput"
|
| 1944 |
},
|
|
|
|
| 1946 |
"name": "rswa_8k_bounded",
|
| 1947 |
"passed": true,
|
| 1948 |
"actual": {
|
| 1949 |
+
"pass_conditions": {
|
| 1950 |
+
"cache_bounded": true,
|
| 1951 |
+
"tps_stable": true,
|
| 1952 |
+
"8k_test_passed": true
|
| 1953 |
+
},
|
| 1954 |
+
"tokens": 8192,
|
| 1955 |
+
"repetition_rate": 0.20496894409937888
|
| 1956 |
},
|
| 1957 |
+
"limit": {
|
| 1958 |
+
"min_tokens": 8192,
|
| 1959 |
+
"max_repetition_rate": 0.25
|
| 1960 |
+
},
|
| 1961 |
+
"detail": "8K generation must have bounded cache, stable throughput, and repetition"
|
| 1962 |
+
},
|
| 1963 |
+
{
|
| 1964 |
+
"name": "provenance_matches_release",
|
| 1965 |
+
"passed": true,
|
| 1966 |
+
"actual": {
|
| 1967 |
+
"source": "baidu/Unlimited-OCR",
|
| 1968 |
+
"reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 1969 |
+
"target": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 1970 |
+
"source_config": {
|
| 1971 |
+
"path": "config.json",
|
| 1972 |
+
"sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9"
|
| 1973 |
+
},
|
| 1974 |
+
"source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 1975 |
+
"reference_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 1976 |
+
"dataset_digest": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
|
| 1977 |
+
"calibration_dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb"
|
| 1978 |
+
},
|
| 1979 |
+
"limit": {
|
| 1980 |
+
"source": "baidu/Unlimited-OCR",
|
| 1981 |
+
"reference": "sahilchachra/unlimited-ocr-mxfp8-mlx",
|
| 1982 |
+
"target": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 1983 |
+
"source_config_sha256": "27246d03fd670904ec9601b1cb0861fbb79ec076830771daa8d943d6229946f9",
|
| 1984 |
+
"source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 1985 |
+
"reference_revision": "55b8031a6c867de675279d9604e38cc94b9882a4",
|
| 1986 |
+
"dataset_digest": "7becb54d81bae0dc69092ff69bae4a324ab63dd672123e5de5d19b6baebbc987",
|
| 1987 |
+
"calibration_dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb"
|
| 1988 |
+
},
|
| 1989 |
+
"detail": "Provenance must bind the source, selection/evaluation datasets, and target"
|
| 1990 |
+
},
|
| 1991 |
+
{
|
| 1992 |
+
"name": "calibration_recomputed",
|
| 1993 |
+
"passed": true,
|
| 1994 |
+
"actual": {
|
| 1995 |
+
"selected": {
|
| 1996 |
+
"label": "affine8-head",
|
| 1997 |
+
"precision": "affine8"
|
| 1998 |
+
},
|
| 1999 |
+
"models": [
|
| 2000 |
+
"AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-affine8",
|
| 2001 |
+
"AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-bfloat16",
|
| 2002 |
+
"AX-Unlimited-OCR-3B-MoE-MLX-MXFP8-cal-mxfp8"
|
| 2003 |
+
],
|
| 2004 |
+
"dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 2005 |
+
"input_artifacts": {
|
| 2006 |
+
"calibration_baseline_accuracy": {
|
| 2007 |
+
"filename": "calibration_baseline_accuracy.json",
|
| 2008 |
+
"size": 5680,
|
| 2009 |
+
"sha256": "ea66cc3190566173dd29c3da3c624b3c950487ebe7b80375d0b159fe46f58cd6"
|
| 2010 |
+
},
|
| 2011 |
+
"calibration_reference_performance": {
|
| 2012 |
+
"filename": "calibration_reference_performance.json",
|
| 2013 |
+
"size": 2271,
|
| 2014 |
+
"sha256": "98b8082733af986c054d2d888722bdc28eb92c6accbb3d1d0b70cd2cffc0aae4"
|
| 2015 |
+
},
|
| 2016 |
+
"calibration_bfloat16_accuracy": {
|
| 2017 |
+
"filename": "calibration_bfloat16_accuracy.json",
|
| 2018 |
+
"size": 5678,
|
| 2019 |
+
"sha256": "5c124b0ffbc8cda32bc941e54b86d8e83f48f9ab05377e1d63b262909b8b1de0"
|
| 2020 |
+
},
|
| 2021 |
+
"calibration_bfloat16_performance": {
|
| 2022 |
+
"filename": "calibration_bfloat16_performance.json",
|
| 2023 |
+
"size": 2244,
|
| 2024 |
+
"sha256": "dba85444f023def084fcd41fb1a404a18010a003557f85bb2070cfda413ec2b0"
|
| 2025 |
+
},
|
| 2026 |
+
"calibration_mxfp8_accuracy": {
|
| 2027 |
+
"filename": "calibration_mxfp8_accuracy.json",
|
| 2028 |
+
"size": 5689,
|
| 2029 |
+
"sha256": "77cbe174170ea41896d26176b30a0320754eef8f62977c08f03f41e161659d26"
|
| 2030 |
+
},
|
| 2031 |
+
"calibration_mxfp8_performance": {
|
| 2032 |
+
"filename": "calibration_mxfp8_performance.json",
|
| 2033 |
+
"size": 2252,
|
| 2034 |
+
"sha256": "e6ad66839447d8b24b7c549fb644a547863a6277ba80d392b9d74572c420099b"
|
| 2035 |
+
},
|
| 2036 |
+
"calibration_affine8_accuracy": {
|
| 2037 |
+
"filename": "calibration_affine8_accuracy.json",
|
| 2038 |
+
"size": 5657,
|
| 2039 |
+
"sha256": "c1f7d2ba7a447c2fb14156e3fabda97b7577035d79bf114ea8ccacec3771ac19"
|
| 2040 |
+
},
|
| 2041 |
+
"calibration_affine8_performance": {
|
| 2042 |
+
"filename": "calibration_affine8_performance.json",
|
| 2043 |
+
"size": 2247,
|
| 2044 |
+
"sha256": "f1c8b7d22cc691797f9e221784f2a747386ea81c5ee1286bcd2860a1670bf89f"
|
| 2045 |
+
}
|
| 2046 |
+
}
|
| 2047 |
+
},
|
| 2048 |
+
"limit": "content-addressed inputs and fastest passing experiment",
|
| 2049 |
+
"detail": "Calibration claims must recompute from raw benchmark evidence"
|
| 2050 |
+
},
|
| 2051 |
+
{
|
| 2052 |
+
"name": "sensitivity_matches_calibration_dataset",
|
| 2053 |
+
"passed": true,
|
| 2054 |
+
"actual": {
|
| 2055 |
+
"dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 2056 |
+
"baseline_metrics_match": true
|
| 2057 |
+
},
|
| 2058 |
+
"limit": {
|
| 2059 |
+
"dataset_digest": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 2060 |
+
"baseline_metrics_match": true
|
| 2061 |
+
},
|
| 2062 |
+
"detail": "Sensitivity decisions must use the recorded selection dataset and baseline"
|
| 2063 |
+
},
|
| 2064 |
+
{
|
| 2065 |
+
"name": "candidate_precision_map_matches_evidence",
|
| 2066 |
+
"passed": true,
|
| 2067 |
+
"actual": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75",
|
| 2068 |
+
"limit": "91163d306379c513935c877d53344c628d333cf4eba2f75f0f5f00889affcd75",
|
| 2069 |
+
"detail": "The executable candidate precision map must equal approved evidence"
|
| 2070 |
+
},
|
| 2071 |
+
{
|
| 2072 |
+
"name": "precision_map_reproducible",
|
| 2073 |
+
"passed": true,
|
| 2074 |
+
"actual": "c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a",
|
| 2075 |
+
"limit": "c5f8b260d2e1ff2fc0405c6ef3857829ca85f49b1980a8c599fb03e06788eb1a",
|
| 2076 |
+
"detail": "Sensitivity and calibration evidence must reproduce the executable map"
|
| 2077 |
}
|
| 2078 |
],
|
| 2079 |
"environment": {
|
release/sensitivity_results.json
CHANGED
|
@@ -1,16 +1,134 @@
|
|
| 1 |
{
|
| 2 |
"baseline": {
|
| 3 |
-
"num_samples":
|
| 4 |
-
"num_digit_samples":
|
| 5 |
"num_table_samples": 3,
|
| 6 |
-
"mean_cer": 0.
|
| 7 |
-
"mean_digit_cer": 0.
|
| 8 |
"mean_table_score": 1.0,
|
| 9 |
-
"total_inference_time":
|
| 10 |
-
"avg_time_per_image":
|
| 11 |
},
|
| 12 |
"model_path": "baidu/Unlimited-OCR",
|
| 13 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"prompt": "<image>document parsing.",
|
| 15 |
"max_tokens": 1024,
|
| 16 |
"groups_tested": [
|
|
@@ -178,17 +296,17 @@
|
|
| 178 |
"vision_model.transformer.layers.9.self_attn.qkv_proj"
|
| 179 |
],
|
| 180 |
"metrics": {
|
| 181 |
-
"num_samples":
|
| 182 |
-
"num_digit_samples":
|
| 183 |
"num_table_samples": 3,
|
| 184 |
-
"mean_cer": 2.
|
| 185 |
-
"mean_digit_cer": 1.
|
| 186 |
"mean_table_score": 0.0,
|
| 187 |
-
"total_inference_time":
|
| 188 |
-
"avg_time_per_image":
|
| 189 |
},
|
| 190 |
-
"cer_delta": 2.
|
| 191 |
-
"digit_cer_delta": 1.
|
| 192 |
"table_score_degradation": 1.0,
|
| 193 |
"sensitivity_rank": 1
|
| 194 |
},
|
|
@@ -199,19 +317,19 @@
|
|
| 199 |
"projector.layers"
|
| 200 |
],
|
| 201 |
"metrics": {
|
| 202 |
-
"num_samples":
|
| 203 |
-
"num_digit_samples":
|
| 204 |
"num_table_samples": 3,
|
| 205 |
-
"mean_cer": 0.
|
| 206 |
-
"mean_digit_cer": 0.
|
| 207 |
"mean_table_score": 1.0,
|
| 208 |
-
"total_inference_time":
|
| 209 |
-
"avg_time_per_image":
|
| 210 |
},
|
| 211 |
-
"cer_delta":
|
| 212 |
-
"digit_cer_delta":
|
| 213 |
"table_score_degradation": 0.0,
|
| 214 |
-
"sensitivity_rank":
|
| 215 |
},
|
| 216 |
"token_embeddings": {
|
| 217 |
"status": "success",
|
|
@@ -220,17 +338,17 @@
|
|
| 220 |
"language_model.model.embed_tokens"
|
| 221 |
],
|
| 222 |
"metrics": {
|
| 223 |
-
"num_samples":
|
| 224 |
-
"num_digit_samples":
|
| 225 |
"num_table_samples": 3,
|
| 226 |
-
"mean_cer": 0.
|
| 227 |
-
"mean_digit_cer": 0.
|
| 228 |
"mean_table_score": 1.0,
|
| 229 |
-
"total_inference_time":
|
| 230 |
-
"avg_time_per_image":
|
| 231 |
},
|
| 232 |
-
"cer_delta": -0.
|
| 233 |
-
"digit_cer_delta": -0.
|
| 234 |
"table_score_degradation": 0.0,
|
| 235 |
"sensitivity_rank": 10
|
| 236 |
},
|
|
@@ -252,17 +370,17 @@
|
|
| 252 |
"language_model.model.layers.9.self_attn.q_proj"
|
| 253 |
],
|
| 254 |
"metrics": {
|
| 255 |
-
"num_samples":
|
| 256 |
-
"num_digit_samples":
|
| 257 |
"num_table_samples": 3,
|
| 258 |
-
"mean_cer": 0.
|
| 259 |
-
"mean_digit_cer": 0.
|
| 260 |
"mean_table_score": 1.0,
|
| 261 |
-
"total_inference_time":
|
| 262 |
-
"avg_time_per_image":
|
| 263 |
},
|
| 264 |
-
"cer_delta": -0.
|
| 265 |
-
"digit_cer_delta": 0.
|
| 266 |
"table_score_degradation": 0.0,
|
| 267 |
"sensitivity_rank": 11
|
| 268 |
},
|
|
@@ -284,17 +402,17 @@
|
|
| 284 |
"language_model.model.layers.9.self_attn.k_proj"
|
| 285 |
],
|
| 286 |
"metrics": {
|
| 287 |
-
"num_samples":
|
| 288 |
-
"num_digit_samples":
|
| 289 |
"num_table_samples": 3,
|
| 290 |
-
"mean_cer": 0.
|
| 291 |
-
"mean_digit_cer": 0.
|
| 292 |
"mean_table_score": 1.0,
|
| 293 |
-
"total_inference_time":
|
| 294 |
-
"avg_time_per_image":
|
| 295 |
},
|
| 296 |
-
"cer_delta": -0.
|
| 297 |
-
"digit_cer_delta":
|
| 298 |
"table_score_degradation": 0.0,
|
| 299 |
"sensitivity_rank": 9
|
| 300 |
},
|
|
@@ -316,19 +434,19 @@
|
|
| 316 |
"language_model.model.layers.9.self_attn.v_proj"
|
| 317 |
],
|
| 318 |
"metrics": {
|
| 319 |
-
"num_samples":
|
| 320 |
-
"num_digit_samples":
|
| 321 |
"num_table_samples": 3,
|
| 322 |
-
"mean_cer": 0.
|
| 323 |
-
"mean_digit_cer": 0.
|
| 324 |
"mean_table_score": 1.0,
|
| 325 |
-
"total_inference_time":
|
| 326 |
-
"avg_time_per_image":
|
| 327 |
},
|
| 328 |
-
"cer_delta": -0.
|
| 329 |
-
"digit_cer_delta":
|
| 330 |
"table_score_degradation": 0.0,
|
| 331 |
-
"sensitivity_rank":
|
| 332 |
},
|
| 333 |
"attention_o_proj": {
|
| 334 |
"status": "success",
|
|
@@ -348,19 +466,19 @@
|
|
| 348 |
"language_model.model.layers.9.self_attn.o_proj"
|
| 349 |
],
|
| 350 |
"metrics": {
|
| 351 |
-
"num_samples":
|
| 352 |
-
"num_digit_samples":
|
| 353 |
"num_table_samples": 3,
|
| 354 |
-
"mean_cer": 0.
|
| 355 |
-
"mean_digit_cer": 0.
|
| 356 |
"mean_table_score": 1.0,
|
| 357 |
-
"total_inference_time":
|
| 358 |
-
"avg_time_per_image":
|
| 359 |
},
|
| 360 |
-
"cer_delta":
|
| 361 |
-
"digit_cer_delta": -0.
|
| 362 |
"table_score_degradation": 0.0,
|
| 363 |
-
"sensitivity_rank":
|
| 364 |
},
|
| 365 |
"dense_mlp": {
|
| 366 |
"status": "success",
|
|
@@ -371,19 +489,19 @@
|
|
| 371 |
"language_model.model.layers.0.mlp.up_proj"
|
| 372 |
],
|
| 373 |
"metrics": {
|
| 374 |
-
"num_samples":
|
| 375 |
-
"num_digit_samples":
|
| 376 |
"num_table_samples": 3,
|
| 377 |
-
"mean_cer": 0.
|
| 378 |
-
"mean_digit_cer": 0.
|
| 379 |
"mean_table_score": 1.0,
|
| 380 |
-
"total_inference_time":
|
| 381 |
-
"avg_time_per_image":
|
| 382 |
},
|
| 383 |
-
"cer_delta": -0.
|
| 384 |
-
"digit_cer_delta":
|
| 385 |
"table_score_degradation": 0.0,
|
| 386 |
-
"sensitivity_rank":
|
| 387 |
},
|
| 388 |
"shared_experts": {
|
| 389 |
"status": "success",
|
|
@@ -424,19 +542,19 @@
|
|
| 424 |
"language_model.model.layers.9.mlp.shared_experts.up_proj"
|
| 425 |
],
|
| 426 |
"metrics": {
|
| 427 |
-
"num_samples":
|
| 428 |
-
"num_digit_samples":
|
| 429 |
"num_table_samples": 3,
|
| 430 |
-
"mean_cer": 0.
|
| 431 |
-
"mean_digit_cer": 0.
|
| 432 |
"mean_table_score": 1.0,
|
| 433 |
-
"total_inference_time":
|
| 434 |
-
"avg_time_per_image":
|
| 435 |
},
|
| 436 |
-
"cer_delta": -0.
|
| 437 |
-
"digit_cer_delta": 0.
|
| 438 |
"table_score_degradation": 0.0,
|
| 439 |
-
"sensitivity_rank":
|
| 440 |
},
|
| 441 |
"routed_experts": {
|
| 442 |
"status": "success",
|
|
@@ -477,19 +595,19 @@
|
|
| 477 |
"language_model.model.layers.9.mlp.switch_mlp.up_proj"
|
| 478 |
],
|
| 479 |
"metrics": {
|
| 480 |
-
"num_samples":
|
| 481 |
-
"num_digit_samples":
|
| 482 |
"num_table_samples": 3,
|
| 483 |
-
"mean_cer": 0.
|
| 484 |
-
"mean_digit_cer": 0.
|
| 485 |
"mean_table_score": 1.0,
|
| 486 |
-
"total_inference_time":
|
| 487 |
-
"avg_time_per_image":
|
| 488 |
},
|
| 489 |
-
"cer_delta":
|
| 490 |
-
"digit_cer_delta": -0.
|
| 491 |
"table_score_degradation": 0.0,
|
| 492 |
-
"sensitivity_rank":
|
| 493 |
},
|
| 494 |
"lm_head": {
|
| 495 |
"status": "success",
|
|
@@ -498,19 +616,19 @@
|
|
| 498 |
"language_model.lm_head"
|
| 499 |
],
|
| 500 |
"metrics": {
|
| 501 |
-
"num_samples":
|
| 502 |
-
"num_digit_samples":
|
| 503 |
"num_table_samples": 3,
|
| 504 |
-
"mean_cer": 0.
|
| 505 |
-
"mean_digit_cer": 0.
|
| 506 |
"mean_table_score": 1.0,
|
| 507 |
-
"total_inference_time":
|
| 508 |
-
"avg_time_per_image":
|
| 509 |
},
|
| 510 |
-
"cer_delta": 0.
|
| 511 |
-
"digit_cer_delta": 0.
|
| 512 |
"table_score_degradation": 0.0,
|
| 513 |
-
"sensitivity_rank":
|
| 514 |
}
|
| 515 |
}
|
| 516 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
"baseline": {
|
| 3 |
+
"num_samples": 10,
|
| 4 |
+
"num_digit_samples": 10,
|
| 5 |
"num_table_samples": 3,
|
| 6 |
+
"mean_cer": 0.20230911688823577,
|
| 7 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 8 |
"mean_table_score": 1.0,
|
| 9 |
+
"total_inference_time": 126.64344700002403,
|
| 10 |
+
"avg_time_per_image": 12.664344700002403
|
| 11 |
},
|
| 12 |
"model_path": "baidu/Unlimited-OCR",
|
| 13 |
+
"source_revision": "ee63731b6461c8afcdcc7b15352e7d2ffecc2ead",
|
| 14 |
+
"eval_dir": "calibration",
|
| 15 |
+
"dataset": {
|
| 16 |
+
"name": "calibration",
|
| 17 |
+
"num_samples": 10,
|
| 18 |
+
"num_digit_samples": 10,
|
| 19 |
+
"num_cjk_samples": 3,
|
| 20 |
+
"num_table_samples": 3,
|
| 21 |
+
"category_counts": {
|
| 22 |
+
"cjk_document": 3,
|
| 23 |
+
"english_document": 2,
|
| 24 |
+
"low_quality_scan": 2,
|
| 25 |
+
"table_numeric": 3
|
| 26 |
+
},
|
| 27 |
+
"manifest_sha256": "d4429669fe60a5c48ce82073294d20c845b85084644f9d4f62f7c8ca3923ba7a",
|
| 28 |
+
"content_sha256": "40260fc715daa552e7b7dcd75d82441065710eccc8b90892b18826cad6f8dfcb",
|
| 29 |
+
"files": [
|
| 30 |
+
{
|
| 31 |
+
"path": "images/0001.png",
|
| 32 |
+
"size": 243645,
|
| 33 |
+
"sha256": "8fad74c7e8f314075f34d5b3b3e6272a2c22a90e960dcf8074039b1f065895b4"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"path": "ground_truth/0001.txt",
|
| 37 |
+
"size": 1346,
|
| 38 |
+
"sha256": "a6f35a418836e8ba0667dc42d663219b07b65cbbf7a4627b2319f0fa44d11f8e"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"path": "images/0002.png",
|
| 42 |
+
"size": 208699,
|
| 43 |
+
"sha256": "31f2d9c651112e963366e5e39f3da4ff8198ec41e2187c3c8fb5fec0edd2f64c"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"path": "ground_truth/0002.txt",
|
| 47 |
+
"size": 838,
|
| 48 |
+
"sha256": "a05196b747f6898102fc56e23cae31c32754b985bc3eab0abe60d955bfadff68"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"path": "images/0003.png",
|
| 52 |
+
"size": 40039,
|
| 53 |
+
"sha256": "bbe26dc81fb4c5e6a143279dd1e0d0540df746f5e4e8f9fc7577e936882b190e"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"path": "ground_truth/0003.txt",
|
| 57 |
+
"size": 459,
|
| 58 |
+
"sha256": "39cf88aef99b6ce6b5d8af870e2feefca7a9991e5c835660bda60c5b5f80f481"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"path": "images/0004.png",
|
| 62 |
+
"size": 40356,
|
| 63 |
+
"sha256": "1e50bec7880f37e9e25eb8c4706998634b0c1f254d38fb75ccd6dbb9b89ed3a3"
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"path": "ground_truth/0004.txt",
|
| 67 |
+
"size": 459,
|
| 68 |
+
"sha256": "2a2b723c4f1b4e14cdf5d70a74ae12586c12952ffac0d1bd295c448f1bf2d3fc"
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"path": "images/0005.png",
|
| 72 |
+
"size": 40650,
|
| 73 |
+
"sha256": "7b7c9441ff17bb97ee76cb68eb6b9134f7d26d151c6ca1d18039959bf43de04c"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"path": "ground_truth/0005.txt",
|
| 77 |
+
"size": 459,
|
| 78 |
+
"sha256": "3dbec98a34736b424dafcf492691b21a1a36f629d83589cac5482cc1a135d9b7"
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"path": "images/0006.png",
|
| 82 |
+
"size": 46036,
|
| 83 |
+
"sha256": "76006318e5aa3bb54182d7b3249ad016e7ab0dee387f580331c39b9ac49cb7ab"
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"path": "ground_truth/0006.txt",
|
| 87 |
+
"size": 191,
|
| 88 |
+
"sha256": "75082837c3cbb8c41ba0747b98390a1f44577092c30f31f4c1e10ce52af83b61"
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"path": "images/0007.png",
|
| 92 |
+
"size": 41828,
|
| 93 |
+
"sha256": "d642986c12233efe0bc7559096952e66eb175735f89ac91caedea0089c4adb19"
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"path": "ground_truth/0007.txt",
|
| 97 |
+
"size": 195,
|
| 98 |
+
"sha256": "c47bf79076b7ab732c22dc1b023d9f6f12b98b724dd0d730c23e879ef7604e29"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"path": "images/0008.png",
|
| 102 |
+
"size": 20975,
|
| 103 |
+
"sha256": "852416613d0137144df77408c44e7d469249e7fbc650c684ad28911b96b28e76"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"path": "ground_truth/0008.txt",
|
| 107 |
+
"size": 201,
|
| 108 |
+
"sha256": "dc48f9c80e21438d5c2c3fb57b3bc471961708e2dd971df78a9ecc11c8c3b3ad"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"path": "images/0009.png",
|
| 112 |
+
"size": 487313,
|
| 113 |
+
"sha256": "87f977973bddf89f781762c87feb9c845e02731dae5cf39086db4cdb0588298e"
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"path": "ground_truth/0009.txt",
|
| 117 |
+
"size": 121,
|
| 118 |
+
"sha256": "d9f04e21b8e73f964a9dee2776c65a76d509138f8abe694c46305a32833a52f1"
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"path": "images/0010.png",
|
| 122 |
+
"size": 515820,
|
| 123 |
+
"sha256": "6fb40a4c63aed4cbb11346d0c80b917cf1ecd407b364bcfbc87483b5bd90d0b4"
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"path": "ground_truth/0010.txt",
|
| 127 |
+
"size": 121,
|
| 128 |
+
"sha256": "b8e07f309069b395279995af437f82f3a9453f2add587bd9633b4ae97cc8419a"
|
| 129 |
+
}
|
| 130 |
+
]
|
| 131 |
+
},
|
| 132 |
"prompt": "<image>document parsing.",
|
| 133 |
"max_tokens": 1024,
|
| 134 |
"groups_tested": [
|
|
|
|
| 296 |
"vision_model.transformer.layers.9.self_attn.qkv_proj"
|
| 297 |
],
|
| 298 |
"metrics": {
|
| 299 |
+
"num_samples": 10,
|
| 300 |
+
"num_digit_samples": 10,
|
| 301 |
"num_table_samples": 3,
|
| 302 |
+
"mean_cer": 2.4745422809406823,
|
| 303 |
+
"mean_digit_cer": 1.7688242659575963,
|
| 304 |
"mean_table_score": 0.0,
|
| 305 |
+
"total_inference_time": 310.5234005409875,
|
| 306 |
+
"avg_time_per_image": 31.05234005409875
|
| 307 |
},
|
| 308 |
+
"cer_delta": 2.2722331640524467,
|
| 309 |
+
"digit_cer_delta": 1.6964543149172047,
|
| 310 |
"table_score_degradation": 1.0,
|
| 311 |
"sensitivity_rank": 1
|
| 312 |
},
|
|
|
|
| 317 |
"projector.layers"
|
| 318 |
],
|
| 319 |
"metrics": {
|
| 320 |
+
"num_samples": 10,
|
| 321 |
+
"num_digit_samples": 10,
|
| 322 |
"num_table_samples": 3,
|
| 323 |
+
"mean_cer": 0.20560582018493906,
|
| 324 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 325 |
"mean_table_score": 1.0,
|
| 326 |
+
"total_inference_time": 125.59270616796857,
|
| 327 |
+
"avg_time_per_image": 12.559270616796857
|
| 328 |
},
|
| 329 |
+
"cer_delta": 0.003296703296703285,
|
| 330 |
+
"digit_cer_delta": 0.0,
|
| 331 |
"table_score_degradation": 0.0,
|
| 332 |
+
"sensitivity_rank": 3
|
| 333 |
},
|
| 334 |
"token_embeddings": {
|
| 335 |
"status": "success",
|
|
|
|
| 338 |
"language_model.model.embed_tokens"
|
| 339 |
],
|
| 340 |
"metrics": {
|
| 341 |
+
"num_samples": 10,
|
| 342 |
+
"num_digit_samples": 10,
|
| 343 |
"num_table_samples": 3,
|
| 344 |
+
"mean_cer": 0.19852784584239075,
|
| 345 |
+
"mean_digit_cer": 0.07026468788249693,
|
| 346 |
"mean_table_score": 1.0,
|
| 347 |
+
"total_inference_time": 125.69877116801217,
|
| 348 |
+
"avg_time_per_image": 12.569877116801218
|
| 349 |
},
|
| 350 |
+
"cer_delta": -0.003781271045845025,
|
| 351 |
+
"digit_cer_delta": -0.002105263157894749,
|
| 352 |
"table_score_degradation": 0.0,
|
| 353 |
"sensitivity_rank": 10
|
| 354 |
},
|
|
|
|
| 370 |
"language_model.model.layers.9.self_attn.q_proj"
|
| 371 |
],
|
| 372 |
"metrics": {
|
| 373 |
+
"num_samples": 10,
|
| 374 |
+
"num_digit_samples": 10,
|
| 375 |
"num_table_samples": 3,
|
| 376 |
+
"mean_cer": 0.1810308435920253,
|
| 377 |
+
"mean_digit_cer": 0.08710679314565484,
|
| 378 |
"mean_table_score": 1.0,
|
| 379 |
+
"total_inference_time": 126.60937520701555,
|
| 380 |
+
"avg_time_per_image": 12.660937520701555
|
| 381 |
},
|
| 382 |
+
"cer_delta": -0.021278273296210487,
|
| 383 |
+
"digit_cer_delta": 0.01473684210526316,
|
| 384 |
"table_score_degradation": 0.0,
|
| 385 |
"sensitivity_rank": 11
|
| 386 |
},
|
|
|
|
| 402 |
"language_model.model.layers.9.self_attn.k_proj"
|
| 403 |
],
|
| 404 |
"metrics": {
|
| 405 |
+
"num_samples": 10,
|
| 406 |
+
"num_digit_samples": 10,
|
| 407 |
"num_table_samples": 3,
|
| 408 |
+
"mean_cer": 0.1995659706473317,
|
| 409 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 410 |
"mean_table_score": 1.0,
|
| 411 |
+
"total_inference_time": 125.29329520699685,
|
| 412 |
+
"avg_time_per_image": 12.529329520699685
|
| 413 |
},
|
| 414 |
+
"cer_delta": -0.0027431462409040763,
|
| 415 |
+
"digit_cer_delta": 0.0,
|
| 416 |
"table_score_degradation": 0.0,
|
| 417 |
"sensitivity_rank": 9
|
| 418 |
},
|
|
|
|
| 434 |
"language_model.model.layers.9.self_attn.v_proj"
|
| 435 |
],
|
| 436 |
"metrics": {
|
| 437 |
+
"num_samples": 10,
|
| 438 |
+
"num_digit_samples": 10,
|
| 439 |
"num_table_samples": 3,
|
| 440 |
+
"mean_cer": 0.20121021578933468,
|
| 441 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 442 |
"mean_table_score": 1.0,
|
| 443 |
+
"total_inference_time": 126.87845074798679,
|
| 444 |
+
"avg_time_per_image": 12.687845074798679
|
| 445 |
},
|
| 446 |
+
"cer_delta": -0.001098901098901095,
|
| 447 |
+
"digit_cer_delta": 0.0,
|
| 448 |
"table_score_degradation": 0.0,
|
| 449 |
+
"sensitivity_rank": 6
|
| 450 |
},
|
| 451 |
"attention_o_proj": {
|
| 452 |
"status": "success",
|
|
|
|
| 466 |
"language_model.model.layers.9.self_attn.o_proj"
|
| 467 |
],
|
| 468 |
"metrics": {
|
| 469 |
+
"num_samples": 10,
|
| 470 |
+
"num_digit_samples": 10,
|
| 471 |
"num_table_samples": 3,
|
| 472 |
+
"mean_cer": 0.2058682899333709,
|
| 473 |
+
"mean_digit_cer": 0.06383873929008568,
|
| 474 |
"mean_table_score": 1.0,
|
| 475 |
+
"total_inference_time": 128.61252554102975,
|
| 476 |
+
"avg_time_per_image": 12.861252554102975
|
| 477 |
},
|
| 478 |
+
"cer_delta": 0.0035591730451351167,
|
| 479 |
+
"digit_cer_delta": -0.008531211750305995,
|
| 480 |
"table_score_degradation": 0.0,
|
| 481 |
+
"sensitivity_rank": 2
|
| 482 |
},
|
| 483 |
"dense_mlp": {
|
| 484 |
"status": "success",
|
|
|
|
| 489 |
"language_model.model.layers.0.mlp.up_proj"
|
| 490 |
],
|
| 491 |
"metrics": {
|
| 492 |
+
"num_samples": 10,
|
| 493 |
+
"num_digit_samples": 10,
|
| 494 |
"num_table_samples": 3,
|
| 495 |
+
"mean_cer": 0.20121021578933468,
|
| 496 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 497 |
"mean_table_score": 1.0,
|
| 498 |
+
"total_inference_time": 127.19930095798918,
|
| 499 |
+
"avg_time_per_image": 12.719930095798919
|
| 500 |
},
|
| 501 |
+
"cer_delta": -0.001098901098901095,
|
| 502 |
+
"digit_cer_delta": 0.0,
|
| 503 |
"table_score_degradation": 0.0,
|
| 504 |
+
"sensitivity_rank": 7
|
| 505 |
},
|
| 506 |
"shared_experts": {
|
| 507 |
"status": "success",
|
|
|
|
| 542 |
"language_model.model.layers.9.mlp.shared_experts.up_proj"
|
| 543 |
],
|
| 544 |
"metrics": {
|
| 545 |
+
"num_samples": 10,
|
| 546 |
+
"num_digit_samples": 10,
|
| 547 |
"num_table_samples": 3,
|
| 548 |
+
"mean_cer": 0.20009091014419536,
|
| 549 |
+
"mean_digit_cer": 0.08267594859241126,
|
| 550 |
"mean_table_score": 1.0,
|
| 551 |
+
"total_inference_time": 128.06347554198874,
|
| 552 |
+
"avg_time_per_image": 12.806347554198874
|
| 553 |
},
|
| 554 |
+
"cer_delta": -0.002218206744040413,
|
| 555 |
+
"digit_cer_delta": 0.01030599755201958,
|
| 556 |
"table_score_degradation": 0.0,
|
| 557 |
+
"sensitivity_rank": 8
|
| 558 |
},
|
| 559 |
"routed_experts": {
|
| 560 |
"status": "success",
|
|
|
|
| 595 |
"language_model.model.layers.9.mlp.switch_mlp.up_proj"
|
| 596 |
],
|
| 597 |
"metrics": {
|
| 598 |
+
"num_samples": 10,
|
| 599 |
+
"num_digit_samples": 10,
|
| 600 |
"num_table_samples": 3,
|
| 601 |
+
"mean_cer": 0.2033268371469385,
|
| 602 |
+
"mean_digit_cer": 0.06899173806609546,
|
| 603 |
"mean_table_score": 1.0,
|
| 604 |
+
"total_inference_time": 125.24760646000504,
|
| 605 |
+
"avg_time_per_image": 12.524760646000505
|
| 606 |
},
|
| 607 |
+
"cer_delta": 0.0010177202587027256,
|
| 608 |
+
"digit_cer_delta": -0.0033782129742962186,
|
| 609 |
"table_score_degradation": 0.0,
|
| 610 |
+
"sensitivity_rank": 5
|
| 611 |
},
|
| 612 |
"lm_head": {
|
| 613 |
"status": "success",
|
|
|
|
| 616 |
"language_model.lm_head"
|
| 617 |
],
|
| 618 |
"metrics": {
|
| 619 |
+
"num_samples": 10,
|
| 620 |
+
"num_digit_samples": 10,
|
| 621 |
"num_table_samples": 3,
|
| 622 |
+
"mean_cer": 0.20560582018493906,
|
| 623 |
+
"mean_digit_cer": 0.07236995104039168,
|
| 624 |
"mean_table_score": 1.0,
|
| 625 |
+
"total_inference_time": 122.19146683299914,
|
| 626 |
+
"avg_time_per_image": 12.219146683299915
|
| 627 |
},
|
| 628 |
+
"cer_delta": 0.003296703296703285,
|
| 629 |
+
"digit_cer_delta": 0.0,
|
| 630 |
"table_score_degradation": 0.0,
|
| 631 |
+
"sensitivity_rank": 4
|
| 632 |
}
|
| 633 |
}
|
| 634 |
+
}
|
src/unlimited_ocr/__init__.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
| 1 |
-
"""Unlimited-OCR
|
| 2 |
|
| 3 |
-
__version__ = "0.
|
| 4 |
|
| 5 |
from unlimited_ocr.pipeline import BatchProcessingError, OCRPipeline
|
| 6 |
from unlimited_ocr.engine import OCREngine
|
| 7 |
from unlimited_ocr.profiles import get_profile, list_profiles, PROFILES
|
|
|
|
| 8 |
|
| 9 |
__all__ = [
|
| 10 |
"OCRPipeline",
|
| 11 |
"BatchProcessingError",
|
| 12 |
"OCREngine",
|
|
|
|
|
|
|
| 13 |
"get_profile",
|
| 14 |
"list_profiles",
|
| 15 |
"PROFILES",
|
|
|
|
| 1 |
+
"""Unlimited-OCR inference toolkit for MLX and CUDA/vLLM."""
|
| 2 |
|
| 3 |
+
__version__ = "0.3.0"
|
| 4 |
|
| 5 |
from unlimited_ocr.pipeline import BatchProcessingError, OCRPipeline
|
| 6 |
from unlimited_ocr.engine import OCREngine
|
| 7 |
from unlimited_ocr.profiles import get_profile, list_profiles, PROFILES
|
| 8 |
+
from unlimited_ocr.vllm_backend import VLLMEngine, VLLMEngineConfig
|
| 9 |
|
| 10 |
__all__ = [
|
| 11 |
"OCRPipeline",
|
| 12 |
"BatchProcessingError",
|
| 13 |
"OCREngine",
|
| 14 |
+
"VLLMEngine",
|
| 15 |
+
"VLLMEngineConfig",
|
| 16 |
"get_profile",
|
| 17 |
"list_profiles",
|
| 18 |
"PROFILES",
|
src/unlimited_ocr/adapter_registry.json
CHANGED
|
@@ -5,12 +5,20 @@
|
|
| 5 |
"table": {
|
| 6 |
"path": "adapters/table-v1",
|
| 7 |
"hf_repo": "AutomatosX/unlimited-ocr-lora-table",
|
| 8 |
-
"description": "Table structure recognition adapter"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
},
|
| 10 |
"receipt": {
|
| 11 |
"path": "adapters/receipt-v1",
|
| 12 |
"hf_repo": "AutomatosX/unlimited-ocr-lora-receipt",
|
| 13 |
-
"description": "Receipt and invoice parsing adapter"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
},
|
| 16 |
"default": null
|
|
|
|
| 5 |
"table": {
|
| 6 |
"path": "adapters/table-v1",
|
| 7 |
"hf_repo": "AutomatosX/unlimited-ocr-lora-table",
|
| 8 |
+
"description": "Table structure recognition adapter",
|
| 9 |
+
"base_model": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 10 |
+
"target_scope": "all supported language-model linear modules except lm_head",
|
| 11 |
+
"lora_rank": 16,
|
| 12 |
+
"scene": "table_numeric"
|
| 13 |
},
|
| 14 |
"receipt": {
|
| 15 |
"path": "adapters/receipt-v1",
|
| 16 |
"hf_repo": "AutomatosX/unlimited-ocr-lora-receipt",
|
| 17 |
+
"description": "Receipt and invoice parsing adapter",
|
| 18 |
+
"base_model": "AutomatosX/AX-Unlimited-OCR-3B-MoE-MLX-MXFP8",
|
| 19 |
+
"target_scope": "all supported language-model linear modules except lm_head",
|
| 20 |
+
"lora_rank": 16,
|
| 21 |
+
"scene": "receipt"
|
| 22 |
}
|
| 23 |
},
|
| 24 |
"default": null
|
src/unlimited_ocr/cli.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Command-line interface for Unlimited-OCR MLX."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
@@ -12,6 +12,7 @@ from unlimited_ocr.engine import DEFAULT_MODEL
|
|
| 12 |
from unlimited_ocr.pdf import DEFAULT_PDF_DPI
|
| 13 |
from unlimited_ocr.preprocessing import PREPROCESS_MODES
|
| 14 |
from unlimited_ocr.profiles import PROFILES
|
|
|
|
| 15 |
|
| 16 |
console = Console()
|
| 17 |
status_console = Console(stderr=True)
|
|
@@ -65,7 +66,11 @@ def _resolve_adapter(name_or_path: str) -> str:
|
|
| 65 |
from huggingface_hub import snapshot_download
|
| 66 |
return snapshot_download(
|
| 67 |
hf_repo,
|
| 68 |
-
allow_patterns=[
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
)
|
| 70 |
except Exception as exc:
|
| 71 |
raise click.ClickException(
|
|
@@ -84,10 +89,11 @@ def _resolve_adapter(name_or_path: str) -> str:
|
|
| 84 |
@click.pass_context
|
| 85 |
@click.version_option(version=__version__, prog_name="unlimited-ocr")
|
| 86 |
def main(ctx):
|
| 87 |
-
"""Unlimited-OCR
|
| 88 |
|
| 89 |
Run OCR on images, PDFs, or entire directories using the Unlimited-OCR
|
| 90 |
-
vision-language model
|
|
|
|
| 91 |
|
| 92 |
\b
|
| 93 |
Examples:
|
|
@@ -123,18 +129,32 @@ def main(ctx):
|
|
| 123 |
help="DPI for PDF rendering (150 is usually enough; model input is 1024px).")
|
| 124 |
@click.option("--max-tokens", type=click.IntRange(min=1), default=None,
|
| 125 |
help="Maximum tokens per page (overrides profile default).")
|
| 126 |
-
@click.option("--
|
| 127 |
-
help="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 138 |
"""Run OCR on an image, PDF, or directory of images.
|
| 139 |
|
| 140 |
INPUT_PATH can be an image file, a PDF file, or a directory of images.
|
|
@@ -154,12 +174,30 @@ def run(input_path, output_path, fmt, task, profile, grounding, preprocess,
|
|
| 154 |
raise click.UsageError("--stream does not support PDF files")
|
| 155 |
if jobs != 1:
|
| 156 |
raise click.UsageError("--jobs only applies to directory batch processing")
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
if verbose:
|
| 161 |
-
status_console.print(f"[bold]Unlimited-OCR
|
| 162 |
-
status_console.print(f"[dim]Model:[/] {
|
| 163 |
status_console.print(f"[dim]Input:[/] {input_path}")
|
| 164 |
status_console.print(f"[dim]Profile:[/] {profile} | Format: {fmt} | Task: {task}")
|
| 165 |
if preprocess:
|
|
@@ -174,17 +212,23 @@ def run(input_path, output_path, fmt, task, profile, grounding, preprocess,
|
|
| 174 |
adapter_path = _resolve_adapter(adapter) if adapter else None
|
| 175 |
|
| 176 |
pipeline = OCRPipeline(
|
| 177 |
-
|
|
|
|
| 178 |
verbose=verbose,
|
| 179 |
preprocess=preprocess,
|
| 180 |
preprocess_mode=preprocess_mode,
|
| 181 |
profile=profile,
|
| 182 |
adapter_path=adapter_path,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
)
|
| 184 |
if input_path.is_dir():
|
| 185 |
# Batch mode
|
| 186 |
_run_batch(pipeline, input_path, output_path, fmt, task,
|
| 187 |
-
grounding, max_tokens, profile, jobs
|
|
|
|
| 188 |
elif stream:
|
| 189 |
# Streaming mode (single file, stdout only)
|
| 190 |
_run_stream(pipeline, input_path, task, grounding, dpi, max_tokens, profile)
|
|
@@ -242,7 +286,8 @@ def _run_single(pipeline, input_path, output_path, fmt, task, grounding, dpi, ma
|
|
| 242 |
click.echo(result)
|
| 243 |
|
| 244 |
|
| 245 |
-
def _run_batch(pipeline, input_dir, output_path, fmt, task, grounding,
|
|
|
|
| 246 |
"""Handle batch directory processing."""
|
| 247 |
results = pipeline.run_batch(
|
| 248 |
input_dir=input_dir,
|
|
@@ -255,6 +300,7 @@ def _run_batch(pipeline, input_dir, output_path, fmt, task, grounding, max_token
|
|
| 255 |
profile=profile,
|
| 256 |
raise_on_error=True,
|
| 257 |
jobs=jobs,
|
|
|
|
| 258 |
)
|
| 259 |
|
| 260 |
if output_path:
|
|
@@ -305,6 +351,9 @@ def models():
|
|
| 305 |
console.print(f" [green]•[/] {DEFAULT_MODEL}")
|
| 306 |
console.print(" MX FP8 quantized (~3.6 GB) — recommended for Apple Silicon")
|
| 307 |
console.print()
|
|
|
|
|
|
|
|
|
|
| 308 |
console.print("[dim]Use --model-path to specify a different model.[/]")
|
| 309 |
|
| 310 |
|
|
@@ -322,6 +371,94 @@ def profiles():
|
|
| 322 |
console.print()
|
| 323 |
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
@main.command()
|
| 326 |
@click.option("--host", type=str, default="127.0.0.1", show_default=True,
|
| 327 |
help="Bind host. Use 0.0.0.0 only to expose the API on your network.")
|
|
|
|
| 1 |
+
"""Command-line interface for Unlimited-OCR MLX and vLLM backends."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 12 |
from unlimited_ocr.pdf import DEFAULT_PDF_DPI
|
| 13 |
from unlimited_ocr.preprocessing import PREPROCESS_MODES
|
| 14 |
from unlimited_ocr.profiles import PROFILES
|
| 15 |
+
from unlimited_ocr.vllm_backend import DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL
|
| 16 |
|
| 17 |
console = Console()
|
| 18 |
status_console = Console(stderr=True)
|
|
|
|
| 66 |
from huggingface_hub import snapshot_download
|
| 67 |
return snapshot_download(
|
| 68 |
hf_repo,
|
| 69 |
+
allow_patterns=[
|
| 70 |
+
"adapter_config.json",
|
| 71 |
+
"adapters.safetensors",
|
| 72 |
+
"training_config.json",
|
| 73 |
+
],
|
| 74 |
)
|
| 75 |
except Exception as exc:
|
| 76 |
raise click.ClickException(
|
|
|
|
| 89 |
@click.pass_context
|
| 90 |
@click.version_option(version=__version__, prog_name="unlimited-ocr")
|
| 91 |
def main(ctx):
|
| 92 |
+
"""Unlimited-OCR — document parsing with MLX or CUDA/vLLM.
|
| 93 |
|
| 94 |
Run OCR on images, PDFs, or entire directories using the Unlimited-OCR
|
| 95 |
+
vision-language model through local MLX or an OpenAI-compatible vLLM
|
| 96 |
+
server on NVIDIA CUDA.
|
| 97 |
|
| 98 |
\b
|
| 99 |
Examples:
|
|
|
|
| 129 |
help="DPI for PDF rendering (150 is usually enough; model input is 1024px).")
|
| 130 |
@click.option("--max-tokens", type=click.IntRange(min=1), default=None,
|
| 131 |
help="Maximum tokens per page (overrides profile default).")
|
| 132 |
+
@click.option("--backend", type=click.Choice(["mlx", "vllm"]), default="mlx",
|
| 133 |
+
show_default=True, help="Inference backend.")
|
| 134 |
+
@click.option("--model-path", type=str, default=None,
|
| 135 |
+
help="Model path/served name. Defaults depend on --backend.")
|
| 136 |
+
@click.option("--vllm-base-url", type=str, default=DEFAULT_VLLM_BASE_URL,
|
| 137 |
+
show_default=True, help="OpenAI-compatible vLLM API root.")
|
| 138 |
+
@click.option("--vllm-api-key", type=str, default=None, envvar="VLLM_API_KEY",
|
| 139 |
+
help="vLLM bearer token (or set VLLM_API_KEY).")
|
| 140 |
+
@click.option("--vllm-timeout", type=click.FloatRange(min=0.001), default=3600.0,
|
| 141 |
+
show_default=True, help="vLLM request timeout in seconds.")
|
| 142 |
+
@click.option("--vllm-verify-tls/--no-vllm-verify-tls", default=True,
|
| 143 |
+
help="Verify TLS certificates for the vLLM endpoint.")
|
| 144 |
@click.option("--verbose", is_flag=True, default=False,
|
| 145 |
help="Enable verbose output.")
|
| 146 |
@click.option("--stream", is_flag=True, default=False,
|
| 147 |
help="Stream output token-by-token (single image only).")
|
| 148 |
@click.option("--jobs", type=click.IntRange(min=1), default=1,
|
| 149 |
help="Parallel preprocessing workers for batch mode.")
|
| 150 |
+
@click.option("--request-concurrency", type=click.IntRange(min=1), default=1,
|
| 151 |
+
help="Concurrent vLLM requests for directory batch mode.")
|
| 152 |
@click.option("--adapter", type=str, default=None,
|
| 153 |
help="LoRA adapter name or path (e.g. 'table' or 'adapters/table-v1').")
|
| 154 |
def run(input_path, output_path, fmt, task, profile, grounding, preprocess,
|
| 155 |
+
preprocess_mode, dpi, max_tokens, backend, model_path, vllm_base_url,
|
| 156 |
+
vllm_api_key, vllm_timeout, vllm_verify_tls, verbose, stream, jobs,
|
| 157 |
+
request_concurrency, adapter):
|
| 158 |
"""Run OCR on an image, PDF, or directory of images.
|
| 159 |
|
| 160 |
INPUT_PATH can be an image file, a PDF file, or a directory of images.
|
|
|
|
| 174 |
raise click.UsageError("--stream does not support PDF files")
|
| 175 |
if jobs != 1:
|
| 176 |
raise click.UsageError("--jobs only applies to directory batch processing")
|
| 177 |
+
if request_concurrency != 1:
|
| 178 |
+
raise click.UsageError(
|
| 179 |
+
"--request-concurrency only applies to directory batch processing"
|
| 180 |
+
)
|
| 181 |
+
elif not input_path.is_dir() and (jobs != 1 or request_concurrency != 1):
|
| 182 |
+
raise click.UsageError(
|
| 183 |
+
"--jobs and --request-concurrency only apply to directory batch processing"
|
| 184 |
+
)
|
| 185 |
+
if request_concurrency > 1 and backend != "vllm":
|
| 186 |
+
raise click.UsageError(
|
| 187 |
+
"--request-concurrency > 1 requires --backend vllm"
|
| 188 |
+
)
|
| 189 |
+
if backend == "vllm" and adapter:
|
| 190 |
+
raise click.UsageError(
|
| 191 |
+
"--adapter is MLX-only; configure LoRA modules when starting vLLM"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
resolved_model = model_path or (
|
| 195 |
+
DEFAULT_VLLM_MODEL if backend == "vllm" else DEFAULT_MODEL
|
| 196 |
+
)
|
| 197 |
|
| 198 |
if verbose:
|
| 199 |
+
status_console.print(f"[bold]Unlimited-OCR {backend.upper()}[/] v{__version__}")
|
| 200 |
+
status_console.print(f"[dim]Model:[/] {resolved_model}")
|
| 201 |
status_console.print(f"[dim]Input:[/] {input_path}")
|
| 202 |
status_console.print(f"[dim]Profile:[/] {profile} | Format: {fmt} | Task: {task}")
|
| 203 |
if preprocess:
|
|
|
|
| 212 |
adapter_path = _resolve_adapter(adapter) if adapter else None
|
| 213 |
|
| 214 |
pipeline = OCRPipeline(
|
| 215 |
+
backend=backend,
|
| 216 |
+
model_path=resolved_model,
|
| 217 |
verbose=verbose,
|
| 218 |
preprocess=preprocess,
|
| 219 |
preprocess_mode=preprocess_mode,
|
| 220 |
profile=profile,
|
| 221 |
adapter_path=adapter_path,
|
| 222 |
+
vllm_base_url=vllm_base_url,
|
| 223 |
+
vllm_api_key=vllm_api_key,
|
| 224 |
+
vllm_timeout=vllm_timeout,
|
| 225 |
+
vllm_verify_tls=vllm_verify_tls,
|
| 226 |
)
|
| 227 |
if input_path.is_dir():
|
| 228 |
# Batch mode
|
| 229 |
_run_batch(pipeline, input_path, output_path, fmt, task,
|
| 230 |
+
grounding, max_tokens, profile, jobs,
|
| 231 |
+
request_concurrency)
|
| 232 |
elif stream:
|
| 233 |
# Streaming mode (single file, stdout only)
|
| 234 |
_run_stream(pipeline, input_path, task, grounding, dpi, max_tokens, profile)
|
|
|
|
| 286 |
click.echo(result)
|
| 287 |
|
| 288 |
|
| 289 |
+
def _run_batch(pipeline, input_dir, output_path, fmt, task, grounding,
|
| 290 |
+
max_tokens, profile, jobs=1, request_concurrency=1):
|
| 291 |
"""Handle batch directory processing."""
|
| 292 |
results = pipeline.run_batch(
|
| 293 |
input_dir=input_dir,
|
|
|
|
| 300 |
profile=profile,
|
| 301 |
raise_on_error=True,
|
| 302 |
jobs=jobs,
|
| 303 |
+
inference_concurrency=request_concurrency,
|
| 304 |
)
|
| 305 |
|
| 306 |
if output_path:
|
|
|
|
| 351 |
console.print(f" [green]•[/] {DEFAULT_MODEL}")
|
| 352 |
console.print(" MX FP8 quantized (~3.6 GB) — recommended for Apple Silicon")
|
| 353 |
console.print()
|
| 354 |
+
console.print(f" [green]•[/] {DEFAULT_VLLM_MODEL}")
|
| 355 |
+
console.print(" BF16 CUDA/vLLM baseline — use an AWQ repo with --model-path when available")
|
| 356 |
+
console.print()
|
| 357 |
console.print("[dim]Use --model-path to specify a different model.[/]")
|
| 358 |
|
| 359 |
|
|
|
|
| 371 |
console.print()
|
| 372 |
|
| 373 |
|
| 374 |
+
@main.command("vllm-serve")
|
| 375 |
+
@click.option("--model-path", type=str, default=DEFAULT_VLLM_MODEL,
|
| 376 |
+
show_default=True, help="Hugging Face repo ID or local CUDA checkpoint.")
|
| 377 |
+
@click.option("--served-model-name", type=str, default=None,
|
| 378 |
+
help="Model name exposed by the OpenAI API (defaults to model path).")
|
| 379 |
+
@click.option("--host", type=str, default="127.0.0.1", show_default=True,
|
| 380 |
+
help="Bind host; protect externally exposed endpoints with --api-key.")
|
| 381 |
+
@click.option("--port", type=click.IntRange(1, 65535), default=8000,
|
| 382 |
+
show_default=True, help="Bind port.")
|
| 383 |
+
@click.option("--tensor-parallel-size", type=click.IntRange(min=1), default=1,
|
| 384 |
+
show_default=True, help="Number of CUDA GPUs used for tensor parallelism.")
|
| 385 |
+
@click.option("--gpu-memory-utilization", type=click.FloatRange(min=0.01, max=1.0),
|
| 386 |
+
default=0.90, show_default=True, help="Per-GPU memory utilization target.")
|
| 387 |
+
@click.option("--max-model-len", type=click.IntRange(min=1), default=32768,
|
| 388 |
+
show_default=True, help="Maximum context length.")
|
| 389 |
+
@click.option("--max-num-seqs", type=click.IntRange(min=1), default=4,
|
| 390 |
+
show_default=True, help="Maximum concurrent sequences.")
|
| 391 |
+
@click.option("--max-images", type=click.IntRange(min=1), default=40,
|
| 392 |
+
show_default=True, help="Maximum images accepted by one request.")
|
| 393 |
+
@click.option("--dtype", type=click.Choice(["auto", "bfloat16", "float16"]),
|
| 394 |
+
default="bfloat16", show_default=True, help="Model compute dtype.")
|
| 395 |
+
@click.option("--revision", type=str, default=None,
|
| 396 |
+
help="Immutable Hugging Face commit revision (recommended for releases).")
|
| 397 |
+
@click.option("--api-key", type=str, default=None, envvar="VLLM_API_KEY",
|
| 398 |
+
help="Protect the OpenAI API with a bearer token (or VLLM_API_KEY).")
|
| 399 |
+
@click.option("--trust-remote-code/--no-trust-remote-code", default=False,
|
| 400 |
+
help="Allow model repository code (not needed for native Unlimited-OCR).")
|
| 401 |
+
@click.option("--dry-run", is_flag=True, default=False,
|
| 402 |
+
help="Print the redacted vLLM command without starting it.")
|
| 403 |
+
def vllm_serve(model_path, served_model_name, host, port,
|
| 404 |
+
tensor_parallel_size, gpu_memory_utilization, max_model_len,
|
| 405 |
+
max_num_seqs, max_images, dtype, revision, api_key,
|
| 406 |
+
trust_remote_code, dry_run):
|
| 407 |
+
"""Start the native CUDA vLLM OpenAI-compatible server.
|
| 408 |
+
|
| 409 |
+
Requires Linux, NVIDIA CUDA, and vLLM 0.25.1+. The command installs the
|
| 410 |
+
Unlimited-OCR R-SWA logits processor and required cache settings.
|
| 411 |
+
"""
|
| 412 |
+
import shlex
|
| 413 |
+
|
| 414 |
+
from unlimited_ocr.vllm_serve import (
|
| 415 |
+
VLLMServeConfig,
|
| 416 |
+
build_vllm_command,
|
| 417 |
+
redact_vllm_command,
|
| 418 |
+
run_vllm_server,
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
config = VLLMServeConfig(
|
| 422 |
+
model_path=model_path,
|
| 423 |
+
served_model_name=served_model_name,
|
| 424 |
+
host=host,
|
| 425 |
+
port=port,
|
| 426 |
+
tensor_parallel_size=tensor_parallel_size,
|
| 427 |
+
gpu_memory_utilization=gpu_memory_utilization,
|
| 428 |
+
max_model_len=max_model_len,
|
| 429 |
+
max_num_seqs=max_num_seqs,
|
| 430 |
+
max_images_per_prompt=max_images,
|
| 431 |
+
dtype=dtype,
|
| 432 |
+
revision=revision,
|
| 433 |
+
api_key=api_key,
|
| 434 |
+
trust_remote_code=trust_remote_code,
|
| 435 |
+
)
|
| 436 |
+
try:
|
| 437 |
+
command = build_vllm_command(config)
|
| 438 |
+
except (TypeError, ValueError) as exc:
|
| 439 |
+
raise click.UsageError(str(exc)) from exc
|
| 440 |
+
|
| 441 |
+
if dry_run:
|
| 442 |
+
rendered = shlex.join(redact_vllm_command(command))
|
| 443 |
+
if api_key:
|
| 444 |
+
rendered = f"VLLM_API_KEY=*** {rendered}"
|
| 445 |
+
click.echo(rendered)
|
| 446 |
+
return
|
| 447 |
+
|
| 448 |
+
if host not in {"127.0.0.1", "localhost", "::1"} and not api_key:
|
| 449 |
+
status_console.print(
|
| 450 |
+
"[yellow]Warning:[/] vLLM is exposed without an API key."
|
| 451 |
+
)
|
| 452 |
+
console.print(f"[bold]Starting Unlimited-OCR CUDA/vLLM[/] on {host}:{port}")
|
| 453 |
+
console.print(f"[dim]Model:[/] {model_path}")
|
| 454 |
+
try:
|
| 455 |
+
exit_code = run_vllm_server(config)
|
| 456 |
+
except Exception as exc:
|
| 457 |
+
raise click.ClickException(str(exc)) from exc
|
| 458 |
+
if exit_code != 0:
|
| 459 |
+
raise click.ClickException(f"vLLM exited with status {exit_code}")
|
| 460 |
+
|
| 461 |
+
|
| 462 |
@main.command()
|
| 463 |
@click.option("--host", type=str, default="127.0.0.1", show_default=True,
|
| 464 |
help="Bind host. Use 0.0.0.0 only to expose the API on your network.")
|
src/unlimited_ocr/engine.py
CHANGED
|
@@ -2,9 +2,10 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import re
|
| 6 |
import time
|
| 7 |
-
import math
|
| 8 |
from numbers import Real
|
| 9 |
from dataclasses import dataclass, field
|
| 10 |
from pathlib import Path
|
|
@@ -34,6 +35,90 @@ PROMPTS = {
|
|
| 34 |
GROUNDING_PREFIX = "<|grounding|>"
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
@dataclass
|
| 38 |
class OCRResult:
|
| 39 |
"""Result from a single OCR inference."""
|
|
@@ -132,24 +217,30 @@ def parse_grounding(text: str) -> list[dict]:
|
|
| 132 |
"text": "",
|
| 133 |
}))
|
| 134 |
|
| 135 |
-
# Current direct form. The first block may omit its opening det token
|
|
|
|
| 136 |
direct_pattern = re.compile(
|
| 137 |
rf"(?:^|<\|det\|>)([^\[\n<]*?)\s*"
|
| 138 |
-
rf"({coordinate_pattern.pattern})\s*<\|/det\|>"
|
| 139 |
r"([^\n<]*)",
|
| 140 |
re.MULTILINE,
|
| 141 |
)
|
| 142 |
for match in direct_pattern.finditer(normalized):
|
| 143 |
if any(start <= match.start() < end for start, end in ref_spans):
|
| 144 |
continue
|
| 145 |
-
|
| 146 |
-
if
|
| 147 |
continue
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
parsed.sort(key=lambda item: item[0])
|
| 155 |
return [item for _, item in parsed]
|
|
@@ -206,6 +297,9 @@ class OCREngine:
|
|
| 206 |
self._model = None
|
| 207 |
self._processor = None
|
| 208 |
|
|
|
|
|
|
|
|
|
|
| 209 |
@property
|
| 210 |
def model(self):
|
| 211 |
"""Lazy-load the MLX model."""
|
|
@@ -222,8 +316,6 @@ class OCREngine:
|
|
| 222 |
|
| 223 |
def _load_model(self):
|
| 224 |
"""Load model and processor from mlx-vlm."""
|
| 225 |
-
from mlx_vlm import load
|
| 226 |
-
|
| 227 |
if self.config.verbose:
|
| 228 |
console.print(f"[bold blue]Loading model:[/] {self.config.model_path}")
|
| 229 |
|
|
@@ -231,9 +323,15 @@ class OCREngine:
|
|
| 231 |
if self.config.adapter_path is not None:
|
| 232 |
if not isinstance(self.config.adapter_path, str) or not self.config.adapter_path:
|
| 233 |
raise ValueError("adapter_path must be a non-empty string or None")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
# mlx-vlm applies adapters to the nested language model correctly;
|
| 235 |
# mlx-lm's helper alone can target the wrong module tree for VLMs.
|
| 236 |
load_kwargs["adapter_path"] = self.config.adapter_path
|
|
|
|
|
|
|
| 237 |
self._model, self._processor = load(self.config.model_path, **load_kwargs)
|
| 238 |
|
| 239 |
if self.config.verbose:
|
|
@@ -252,32 +350,11 @@ class OCREngine:
|
|
| 252 |
grounding: If True, prefix with grounding token for bounding boxes.
|
| 253 |
custom_prompt: Override with a fully custom prompt (must contain <image>).
|
| 254 |
"""
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
raise ValueError(
|
| 261 |
-
"custom_prompt must contain exactly one literal '<image>' token"
|
| 262 |
-
)
|
| 263 |
-
prompt = custom_prompt
|
| 264 |
-
else:
|
| 265 |
-
if task not in PROMPTS:
|
| 266 |
-
available = ", ".join(PROMPTS)
|
| 267 |
-
raise ValueError(f"Unknown task '{task}'. Available: {available}")
|
| 268 |
-
prompt = PROMPTS[task]
|
| 269 |
-
|
| 270 |
-
wants_grounding = grounding or GROUNDING_PREFIX in prompt
|
| 271 |
-
if wants_grounding:
|
| 272 |
-
# Upstream DeepSeek/Unlimited-OCR places grounding after the image
|
| 273 |
-
# marker: ``<image>\n<|grounding|>task``. Normalize misplaced or
|
| 274 |
-
# duplicate copies in custom prompts to that trained format.
|
| 275 |
-
prompt = prompt.replace(GROUNDING_PREFIX, "")
|
| 276 |
-
before, after = prompt.split("<image>", 1)
|
| 277 |
-
after = after.lstrip("\r\n")
|
| 278 |
-
prompt = f"{before}<image>\n{GROUNDING_PREFIX}{after}"
|
| 279 |
-
|
| 280 |
-
return prompt
|
| 281 |
|
| 282 |
def _validate_generation_settings(
|
| 283 |
self,
|
|
@@ -560,6 +637,11 @@ class OCREngine:
|
|
| 560 |
"""Check if the model is currently loaded."""
|
| 561 |
return self._model is not None
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
def unload(self):
|
| 564 |
"""Release model from memory."""
|
| 565 |
self._model = None
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
import re
|
| 8 |
import time
|
|
|
|
| 9 |
from numbers import Real
|
| 10 |
from dataclasses import dataclass, field
|
| 11 |
from pathlib import Path
|
|
|
|
| 35 |
GROUNDING_PREFIX = "<|grounding|>"
|
| 36 |
|
| 37 |
|
| 38 |
+
def _same_model_reference(recorded: str, requested: str) -> bool:
|
| 39 |
+
if recorded == requested:
|
| 40 |
+
return True
|
| 41 |
+
recorded_path = Path(recorded).expanduser()
|
| 42 |
+
requested_path = Path(requested).expanduser()
|
| 43 |
+
return (
|
| 44 |
+
recorded_path.exists()
|
| 45 |
+
and requested_path.exists()
|
| 46 |
+
and recorded_path.resolve() == requested_path.resolve()
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _validate_adapter_base_model(adapter_path: str, model_path: str) -> None:
|
| 51 |
+
"""Fail before MLX load when a local adapter records another base model."""
|
| 52 |
+
adapter_dir = Path(adapter_path).expanduser()
|
| 53 |
+
if not adapter_dir.is_dir():
|
| 54 |
+
return
|
| 55 |
+
config_path = adapter_dir / "training_config.json"
|
| 56 |
+
if not config_path.exists() and not config_path.is_symlink():
|
| 57 |
+
return
|
| 58 |
+
if not config_path.is_file():
|
| 59 |
+
raise ValueError(
|
| 60 |
+
f"Adapter training config must be a readable file: {config_path}"
|
| 61 |
+
)
|
| 62 |
+
try:
|
| 63 |
+
training_config = json.loads(config_path.read_text(encoding="utf-8"))
|
| 64 |
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
| 65 |
+
raise ValueError(f"Adapter has invalid training config: {config_path}") from exc
|
| 66 |
+
if not isinstance(training_config, dict):
|
| 67 |
+
raise ValueError(f"Adapter training config must be an object: {config_path}")
|
| 68 |
+
recorded_base_model = training_config.get("base_model")
|
| 69 |
+
if (
|
| 70 |
+
not isinstance(recorded_base_model, str)
|
| 71 |
+
or not recorded_base_model
|
| 72 |
+
or recorded_base_model != recorded_base_model.strip()
|
| 73 |
+
):
|
| 74 |
+
raise ValueError(
|
| 75 |
+
f"Adapter training config requires a normalized base_model: {config_path}"
|
| 76 |
+
)
|
| 77 |
+
if not _same_model_reference(recorded_base_model, model_path):
|
| 78 |
+
raise ValueError(
|
| 79 |
+
"Adapter base model mismatch: training config records "
|
| 80 |
+
f"{recorded_base_model!r}, but inference requested {model_path!r}"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def build_ocr_prompt(
|
| 85 |
+
task: str = "document",
|
| 86 |
+
grounding: bool = False,
|
| 87 |
+
custom_prompt: Optional[str] = None,
|
| 88 |
+
) -> str:
|
| 89 |
+
"""Build a backend-independent Unlimited-OCR prompt.
|
| 90 |
+
|
| 91 |
+
Both the local MLX engine and the remote vLLM client must use the same
|
| 92 |
+
trained prompt vocabulary. Keeping prompt normalization here prevents the
|
| 93 |
+
two backends from silently producing different requests.
|
| 94 |
+
"""
|
| 95 |
+
if custom_prompt is not None:
|
| 96 |
+
if not isinstance(custom_prompt, str):
|
| 97 |
+
raise TypeError("custom_prompt must be a string")
|
| 98 |
+
image_count = custom_prompt.count("<image>")
|
| 99 |
+
if image_count != 1:
|
| 100 |
+
raise ValueError(
|
| 101 |
+
"custom_prompt must contain exactly one literal '<image>' token"
|
| 102 |
+
)
|
| 103 |
+
prompt = custom_prompt
|
| 104 |
+
else:
|
| 105 |
+
if task not in PROMPTS:
|
| 106 |
+
available = ", ".join(PROMPTS)
|
| 107 |
+
raise ValueError(f"Unknown task '{task}'. Available: {available}")
|
| 108 |
+
prompt = PROMPTS[task]
|
| 109 |
+
|
| 110 |
+
wants_grounding = grounding or GROUNDING_PREFIX in prompt
|
| 111 |
+
if wants_grounding:
|
| 112 |
+
# Upstream DeepSeek/Unlimited-OCR places grounding after the image
|
| 113 |
+
# marker: ``<image>\n<|grounding|>task``.
|
| 114 |
+
prompt = prompt.replace(GROUNDING_PREFIX, "")
|
| 115 |
+
before, after = prompt.split("<image>", 1)
|
| 116 |
+
after = after.lstrip("\r\n")
|
| 117 |
+
prompt = f"{before}<image>\n{GROUNDING_PREFIX}{after}"
|
| 118 |
+
|
| 119 |
+
return prompt
|
| 120 |
+
|
| 121 |
+
|
| 122 |
@dataclass
|
| 123 |
class OCRResult:
|
| 124 |
"""Result from a single OCR inference."""
|
|
|
|
| 217 |
"text": "",
|
| 218 |
}))
|
| 219 |
|
| 220 |
+
# Current direct form. The first block may omit its opening det token, and
|
| 221 |
+
# a single det span may list several boxes for the same label.
|
| 222 |
direct_pattern = re.compile(
|
| 223 |
rf"(?:^|<\|det\|>)([^\[\n<]*?)\s*"
|
| 224 |
+
rf"((?:{coordinate_pattern.pattern}\s*)+)\s*<\|/det\|>"
|
| 225 |
r"([^\n<]*)",
|
| 226 |
re.MULTILINE,
|
| 227 |
)
|
| 228 |
for match in direct_pattern.finditer(normalized):
|
| 229 |
if any(start <= match.start() < end for start, end in ref_spans):
|
| 230 |
continue
|
| 231 |
+
boxes = list(coordinate_pattern.finditer(match.group(2)))
|
| 232 |
+
if not boxes:
|
| 233 |
continue
|
| 234 |
+
label = match.group(1).strip()
|
| 235 |
+
# group(2) is the coordinate run (nested number groups inside it);
|
| 236 |
+
# the trailing content capture is the last group.
|
| 237 |
+
text = match.group(match.lastindex).strip() if match.lastindex else ""
|
| 238 |
+
for coordinate_match in boxes:
|
| 239 |
+
parsed.append((match.start() + coordinate_match.start(), {
|
| 240 |
+
"label": label,
|
| 241 |
+
"bbox": coordinate_from_match(coordinate_match),
|
| 242 |
+
"text": text,
|
| 243 |
+
}))
|
| 244 |
|
| 245 |
parsed.sort(key=lambda item: item[0])
|
| 246 |
return [item for _, item in parsed]
|
|
|
|
| 297 |
self._model = None
|
| 298 |
self._processor = None
|
| 299 |
|
| 300 |
+
backend_name = "mlx"
|
| 301 |
+
supports_multi_image = False
|
| 302 |
+
|
| 303 |
@property
|
| 304 |
def model(self):
|
| 305 |
"""Lazy-load the MLX model."""
|
|
|
|
| 316 |
|
| 317 |
def _load_model(self):
|
| 318 |
"""Load model and processor from mlx-vlm."""
|
|
|
|
|
|
|
| 319 |
if self.config.verbose:
|
| 320 |
console.print(f"[bold blue]Loading model:[/] {self.config.model_path}")
|
| 321 |
|
|
|
|
| 323 |
if self.config.adapter_path is not None:
|
| 324 |
if not isinstance(self.config.adapter_path, str) or not self.config.adapter_path:
|
| 325 |
raise ValueError("adapter_path must be a non-empty string or None")
|
| 326 |
+
_validate_adapter_base_model(
|
| 327 |
+
self.config.adapter_path,
|
| 328 |
+
self.config.model_path,
|
| 329 |
+
)
|
| 330 |
# mlx-vlm applies adapters to the nested language model correctly;
|
| 331 |
# mlx-lm's helper alone can target the wrong module tree for VLMs.
|
| 332 |
load_kwargs["adapter_path"] = self.config.adapter_path
|
| 333 |
+
from mlx_vlm import load
|
| 334 |
+
|
| 335 |
self._model, self._processor = load(self.config.model_path, **load_kwargs)
|
| 336 |
|
| 337 |
if self.config.verbose:
|
|
|
|
| 350 |
grounding: If True, prefix with grounding token for bounding boxes.
|
| 351 |
custom_prompt: Override with a fully custom prompt (must contain <image>).
|
| 352 |
"""
|
| 353 |
+
return build_ocr_prompt(
|
| 354 |
+
task=task,
|
| 355 |
+
grounding=grounding,
|
| 356 |
+
custom_prompt=custom_prompt,
|
| 357 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
def _validate_generation_settings(
|
| 360 |
self,
|
|
|
|
| 637 |
"""Check if the model is currently loaded."""
|
| 638 |
return self._model is not None
|
| 639 |
|
| 640 |
+
def ensure_ready(self) -> None:
|
| 641 |
+
"""Load the local model and processor before accepting work."""
|
| 642 |
+
_ = self.model
|
| 643 |
+
_ = self.processor
|
| 644 |
+
|
| 645 |
def unload(self):
|
| 646 |
"""Release model from memory."""
|
| 647 |
self._model = None
|
src/unlimited_ocr/pdf.py
CHANGED
|
@@ -48,47 +48,53 @@ def _pixmap_to_pil(pixmap: fitz.Pixmap) -> Image.Image:
|
|
| 48 |
|
| 49 |
Pixmaps may be CMYK/gray or carry premultiplied alpha. Reading samples as
|
| 50 |
raw RGB packing (or dropping alpha without compositing) produces near-black
|
| 51 |
-
garbage.
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
"""
|
| 54 |
-
if
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# Older PyMuPDF fallback: force DeviceRGB and copy sample bytes.
|
| 58 |
if pixmap.colorspace is None or pixmap.n - pixmap.alpha != 3:
|
| 59 |
pixmap = fitz.Pixmap(fitz.csRGB, pixmap)
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
|
| 94 |
def iter_pdf_pages(
|
|
@@ -185,9 +191,10 @@ def pdf_to_images(
|
|
| 185 |
for i, page_idx in enumerate(range(start, end + 1)):
|
| 186 |
page = doc[page_idx]
|
| 187 |
pixmap = page.get_pixmap(matrix=mat, alpha=False)
|
| 188 |
-
|
|
|
|
| 189 |
out_path = output_dir / f"page_{page_idx + 1:04d}.png"
|
| 190 |
-
pixmap.save(str(out_path))
|
| 191 |
image_paths.append(out_path)
|
| 192 |
|
| 193 |
if progress_callback:
|
|
|
|
| 48 |
|
| 49 |
Pixmaps may be CMYK/gray or carry premultiplied alpha. Reading samples as
|
| 50 |
raw RGB packing (or dropping alpha without compositing) produces near-black
|
| 51 |
+
garbage. PyMuPDF stores alpha-bearing samples premultiplied, so compositing
|
| 52 |
+
must use ``premul + 255 * (1 - a)`` rather than treating channels as straight
|
| 53 |
+
alpha (PIL's ``paste(..., mask=)`` path). Composite onto white so document
|
| 54 |
+
pages keep a paper background.
|
| 55 |
"""
|
| 56 |
+
if pixmap.alpha:
|
| 57 |
+
# Premultiplied alpha → opaque RGB on white. Do not use pil_image()+paste:
|
| 58 |
+
# that path assumes straight alpha and darkens translucent ink.
|
|
|
|
| 59 |
if pixmap.colorspace is None or pixmap.n - pixmap.alpha != 3:
|
| 60 |
pixmap = fitz.Pixmap(fitz.csRGB, pixmap)
|
| 61 |
+
import array
|
| 62 |
+
|
| 63 |
+
samples = array.array("B", pixmap.samples)
|
| 64 |
+
width, height = pixmap.width, pixmap.height
|
| 65 |
+
stride = pixmap.n
|
| 66 |
+
alpha_index = stride - 1
|
| 67 |
+
rgb = bytearray(width * height * 3)
|
| 68 |
+
for index in range(width * height):
|
| 69 |
+
base = index * stride
|
| 70 |
+
alpha = samples[base + alpha_index] / 255.0
|
| 71 |
+
inv = 1.0 - alpha
|
| 72 |
+
for channel in range(3):
|
| 73 |
+
value = samples[base + channel] + 255.0 * inv
|
| 74 |
+
rgb[index * 3 + channel] = 0 if value < 0 else (
|
| 75 |
+
255 if value > 255 else int(value)
|
| 76 |
+
)
|
| 77 |
+
return Image.frombytes("RGB", (width, height), bytes(rgb))
|
| 78 |
+
|
| 79 |
+
# Opaque path: prefer pil_image so CMYK/gray conversion stays with PyMuPDF.
|
| 80 |
+
if hasattr(pixmap, "pil_image"):
|
| 81 |
+
image = pixmap.pil_image()
|
| 82 |
+
if image.mode == "RGB":
|
| 83 |
+
# Copy so the image outlives the temporary pixmap.
|
| 84 |
+
return image.copy()
|
| 85 |
+
if image.mode == "RGBA":
|
| 86 |
+
background = Image.new("RGB", image.size, (255, 255, 255))
|
| 87 |
+
background.paste(image, mask=image.getchannel("A"))
|
| 88 |
+
return background
|
| 89 |
+
return image.convert("RGB")
|
| 90 |
+
|
| 91 |
+
if pixmap.colorspace is None or pixmap.n != 3:
|
| 92 |
+
pixmap = fitz.Pixmap(fitz.csRGB, pixmap)
|
| 93 |
+
return Image.frombytes(
|
| 94 |
+
"RGB",
|
| 95 |
+
(pixmap.width, pixmap.height),
|
| 96 |
+
bytes(pixmap.samples),
|
| 97 |
+
)
|
| 98 |
|
| 99 |
|
| 100 |
def iter_pdf_pages(
|
|
|
|
| 191 |
for i, page_idx in enumerate(range(start, end + 1)):
|
| 192 |
page = doc[page_idx]
|
| 193 |
pixmap = page.get_pixmap(matrix=mat, alpha=False)
|
| 194 |
+
# Route through the same RGB/alpha normalizer as the in-memory
|
| 195 |
+
# path so unusual page colorspaces cannot fail PNG encoding.
|
| 196 |
out_path = output_dir / f"page_{page_idx + 1:04d}.png"
|
| 197 |
+
_pixmap_to_pil(pixmap).save(str(out_path))
|
| 198 |
image_paths.append(out_path)
|
| 199 |
|
| 200 |
if progress_callback:
|
src/unlimited_ocr/pipeline.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
| 4 |
|
| 5 |
import tempfile
|
| 6 |
from collections import Counter
|
| 7 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 8 |
from dataclasses import replace
|
| 9 |
from pathlib import Path
|
| 10 |
from threading import Lock
|
|
@@ -19,6 +19,12 @@ from unlimited_ocr.output import DocumentResult, format_result, save_result
|
|
| 19 |
from unlimited_ocr.pdf import DEFAULT_PDF_DPI, get_pdf_info, iter_pdf_pages, is_pdf
|
| 20 |
from unlimited_ocr.preprocessing import PreprocessConfig, preprocess_to_pil, config_for_mode
|
| 21 |
from unlimited_ocr.profiles import get_profile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
console = Console(stderr=True)
|
| 24 |
|
|
@@ -52,7 +58,7 @@ class OCRPipeline:
|
|
| 52 |
|
| 53 |
def __init__(
|
| 54 |
self,
|
| 55 |
-
model_path: str =
|
| 56 |
max_tokens: Optional[int] = None,
|
| 57 |
temperature: Optional[float] = None,
|
| 58 |
verbose: bool = False,
|
|
@@ -61,11 +67,18 @@ class OCRPipeline:
|
|
| 61 |
preprocess_config: Optional[PreprocessConfig] = None,
|
| 62 |
profile: str = "accurate",
|
| 63 |
adapter_path: Optional[str] = None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
):
|
| 65 |
"""Initialize the OCR pipeline.
|
| 66 |
|
| 67 |
Args:
|
| 68 |
-
model_path: HuggingFace model ID or local path.
|
|
|
|
| 69 |
max_tokens: Optional per-page override for the selected profile.
|
| 70 |
temperature: Optional sampling-temperature override.
|
| 71 |
verbose: Enable verbose output.
|
|
@@ -74,18 +87,51 @@ class OCRPipeline:
|
|
| 74 |
preprocess_config: Custom preprocessing configuration (overrides mode).
|
| 75 |
profile: Generation profile (accurate/fast/long-document/plain-text/markdown).
|
| 76 |
adapter_path: Optional LoRA adapter path for scene-specific inference.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
"""
|
| 78 |
# Initialize first so cleanup remains safe if later validation raises.
|
| 79 |
self._tmp_dir: Optional[tempfile.TemporaryDirectory] = None
|
| 80 |
self._tmp_lock = Lock()
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
self.preprocess_enabled = preprocess
|
| 90 |
self.preprocess_mode = preprocess_mode
|
| 91 |
self.preprocess_config = preprocess_config or config_for_mode(preprocess_mode)
|
|
@@ -269,6 +315,7 @@ class OCRPipeline:
|
|
| 269 |
profile: Optional[str] = None,
|
| 270 |
raise_on_error: bool = False,
|
| 271 |
jobs: int = 1,
|
|
|
|
| 272 |
memory_reset_interval: int = 50,
|
| 273 |
) -> dict[str, str]:
|
| 274 |
"""Run OCR on all images in a directory.
|
|
@@ -286,6 +333,8 @@ class OCRPipeline:
|
|
| 286 |
raise_on_error: Raise BatchProcessingError after processing if any
|
| 287 |
files failed. Successful results remain attached to the error.
|
| 288 |
jobs: Number of parallel workers for image preprocessing (1=sequential).
|
|
|
|
|
|
|
| 289 |
memory_reset_interval: Reset the reported Metal peak-memory
|
| 290 |
counter every N successful images (0=never).
|
| 291 |
|
|
@@ -298,6 +347,16 @@ class OCRPipeline:
|
|
| 298 |
self._validate_run_options(task, format, profile, max_tokens)
|
| 299 |
if not isinstance(jobs, int) or isinstance(jobs, bool) or jobs < 1:
|
| 300 |
raise ValueError("jobs must be a positive integer")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
if (
|
| 302 |
not isinstance(memory_reset_interval, int)
|
| 303 |
or isinstance(memory_reset_interval, bool)
|
|
@@ -356,6 +415,7 @@ class OCRPipeline:
|
|
| 356 |
preparation_errors[img_file.name] = exc
|
| 357 |
|
| 358 |
processed_count = 0
|
|
|
|
| 359 |
|
| 360 |
def _process_one(img_file: Path) -> tuple[str, str]:
|
| 361 |
"""Process a single image, returns (filename, output_text)."""
|
|
@@ -387,14 +447,47 @@ class OCRPipeline:
|
|
| 387 |
out_path = output_path_for(img_file)
|
| 388 |
save_result(doc_result, out_path, format)
|
| 389 |
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
# Start a fresh peak-memory reporting window. This does not free
|
| 392 |
# model memory; MLX manages allocations itself.
|
| 393 |
-
if
|
| 394 |
self._reset_memory_stats()
|
| 395 |
|
| 396 |
return img_file.name, output
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
if show_progress:
|
| 399 |
with Progress(
|
| 400 |
SpinnerColumn(),
|
|
@@ -404,20 +497,9 @@ class OCRPipeline:
|
|
| 404 |
console=console,
|
| 405 |
) as progress:
|
| 406 |
task_id = progress.add_task("Processing images...", total=len(image_files))
|
| 407 |
-
|
| 408 |
-
try:
|
| 409 |
-
name, output = _process_one(img_file)
|
| 410 |
-
results[name] = output
|
| 411 |
-
except Exception as e:
|
| 412 |
-
errors.append((img_file.name, str(e)))
|
| 413 |
-
progress.advance(task_id)
|
| 414 |
else:
|
| 415 |
-
|
| 416 |
-
try:
|
| 417 |
-
name, output = _process_one(img_file)
|
| 418 |
-
results[name] = output
|
| 419 |
-
except Exception as e:
|
| 420 |
-
errors.append((img_file.name, str(e)))
|
| 421 |
|
| 422 |
# Report errors
|
| 423 |
if errors:
|
|
@@ -427,10 +509,18 @@ class OCRPipeline:
|
|
| 427 |
if raise_on_error:
|
| 428 |
raise BatchProcessingError(errors, results)
|
| 429 |
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
|
| 432 |
def _reset_memory_stats(self):
|
| 433 |
"""Reset MLX's peak-memory statistic for subsequent measurements."""
|
|
|
|
|
|
|
| 434 |
# Avoid importing/initializing MLX merely to reset a statistic when no
|
| 435 |
# model has been loaded (important on non-Metal and headless hosts).
|
| 436 |
if not self.engine.is_loaded():
|
|
@@ -529,7 +619,7 @@ class OCRPipeline:
|
|
| 529 |
)
|
| 530 |
|
| 531 |
def cleanup(self):
|
| 532 |
-
"""Clean up temporary files."""
|
| 533 |
lock = getattr(self, "_tmp_lock", None)
|
| 534 |
if lock is None:
|
| 535 |
return
|
|
@@ -537,6 +627,10 @@ class OCRPipeline:
|
|
| 537 |
if getattr(self, "_tmp_dir", None) is not None:
|
| 538 |
self._tmp_dir.cleanup()
|
| 539 |
self._tmp_dir = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
def _validate_run_options(
|
| 542 |
self,
|
|
|
|
| 4 |
|
| 5 |
import tempfile
|
| 6 |
from collections import Counter
|
| 7 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 8 |
from dataclasses import replace
|
| 9 |
from pathlib import Path
|
| 10 |
from threading import Lock
|
|
|
|
| 19 |
from unlimited_ocr.pdf import DEFAULT_PDF_DPI, get_pdf_info, iter_pdf_pages, is_pdf
|
| 20 |
from unlimited_ocr.preprocessing import PreprocessConfig, preprocess_to_pil, config_for_mode
|
| 21 |
from unlimited_ocr.profiles import get_profile
|
| 22 |
+
from unlimited_ocr.vllm_backend import (
|
| 23 |
+
DEFAULT_VLLM_BASE_URL,
|
| 24 |
+
DEFAULT_VLLM_MODEL,
|
| 25 |
+
VLLMEngine,
|
| 26 |
+
VLLMEngineConfig,
|
| 27 |
+
)
|
| 28 |
|
| 29 |
console = Console(stderr=True)
|
| 30 |
|
|
|
|
| 58 |
|
| 59 |
def __init__(
|
| 60 |
self,
|
| 61 |
+
model_path: Optional[str] = None,
|
| 62 |
max_tokens: Optional[int] = None,
|
| 63 |
temperature: Optional[float] = None,
|
| 64 |
verbose: bool = False,
|
|
|
|
| 67 |
preprocess_config: Optional[PreprocessConfig] = None,
|
| 68 |
profile: str = "accurate",
|
| 69 |
adapter_path: Optional[str] = None,
|
| 70 |
+
backend: str = "mlx",
|
| 71 |
+
vllm_base_url: str = DEFAULT_VLLM_BASE_URL,
|
| 72 |
+
vllm_api_key: Optional[str] = None,
|
| 73 |
+
vllm_timeout: float = 3600.0,
|
| 74 |
+
vllm_verify_tls: bool = True,
|
| 75 |
+
vllm_max_images: int = 40,
|
| 76 |
):
|
| 77 |
"""Initialize the OCR pipeline.
|
| 78 |
|
| 79 |
Args:
|
| 80 |
+
model_path: HuggingFace model ID or local path. Defaults to the
|
| 81 |
+
AutomatosX MLX model for ``mlx`` and upstream BF16 for ``vllm``.
|
| 82 |
max_tokens: Optional per-page override for the selected profile.
|
| 83 |
temperature: Optional sampling-temperature override.
|
| 84 |
verbose: Enable verbose output.
|
|
|
|
| 87 |
preprocess_config: Custom preprocessing configuration (overrides mode).
|
| 88 |
profile: Generation profile (accurate/fast/long-document/plain-text/markdown).
|
| 89 |
adapter_path: Optional LoRA adapter path for scene-specific inference.
|
| 90 |
+
backend: Inference backend (``mlx`` or ``vllm``).
|
| 91 |
+
vllm_base_url: OpenAI-compatible vLLM API root.
|
| 92 |
+
vllm_api_key: Optional vLLM bearer token.
|
| 93 |
+
vllm_timeout: Remote inference timeout in seconds.
|
| 94 |
+
vllm_verify_tls: Verify HTTPS certificates for vLLM requests.
|
| 95 |
+
vllm_max_images: Maximum images in one vLLM request.
|
| 96 |
"""
|
| 97 |
# Initialize first so cleanup remains safe if later validation raises.
|
| 98 |
self._tmp_dir: Optional[tempfile.TemporaryDirectory] = None
|
| 99 |
self._tmp_lock = Lock()
|
| 100 |
+
if not isinstance(backend, str):
|
| 101 |
+
raise TypeError("backend must be a string")
|
| 102 |
+
backend_name = backend.strip().lower()
|
| 103 |
+
if backend_name not in {"mlx", "vllm"}:
|
| 104 |
+
raise ValueError("backend must be one of: mlx, vllm")
|
| 105 |
+
self.backend = backend_name
|
| 106 |
+
if backend_name == "mlx":
|
| 107 |
+
resolved_model = model_path or DEFAULT_MODEL
|
| 108 |
+
self.engine = OCREngine(EngineConfig(
|
| 109 |
+
model_path=resolved_model,
|
| 110 |
+
max_tokens=max_tokens,
|
| 111 |
+
temperature=temperature,
|
| 112 |
+
verbose=verbose,
|
| 113 |
+
profile=profile,
|
| 114 |
+
adapter_path=adapter_path,
|
| 115 |
+
))
|
| 116 |
+
else:
|
| 117 |
+
if adapter_path is not None:
|
| 118 |
+
raise ValueError(
|
| 119 |
+
"adapter_path is not supported by the vLLM client; "
|
| 120 |
+
"configure LoRA modules when starting vLLM"
|
| 121 |
+
)
|
| 122 |
+
resolved_model = model_path or DEFAULT_VLLM_MODEL
|
| 123 |
+
self.engine = VLLMEngine(VLLMEngineConfig(
|
| 124 |
+
model_path=resolved_model,
|
| 125 |
+
base_url=vllm_base_url,
|
| 126 |
+
api_key=vllm_api_key,
|
| 127 |
+
timeout_seconds=vllm_timeout,
|
| 128 |
+
verify_tls=vllm_verify_tls,
|
| 129 |
+
max_tokens=max_tokens,
|
| 130 |
+
temperature=temperature,
|
| 131 |
+
verbose=verbose,
|
| 132 |
+
profile=profile,
|
| 133 |
+
max_images_per_request=vllm_max_images,
|
| 134 |
+
))
|
| 135 |
self.preprocess_enabled = preprocess
|
| 136 |
self.preprocess_mode = preprocess_mode
|
| 137 |
self.preprocess_config = preprocess_config or config_for_mode(preprocess_mode)
|
|
|
|
| 315 |
profile: Optional[str] = None,
|
| 316 |
raise_on_error: bool = False,
|
| 317 |
jobs: int = 1,
|
| 318 |
+
inference_concurrency: int = 1,
|
| 319 |
memory_reset_interval: int = 50,
|
| 320 |
) -> dict[str, str]:
|
| 321 |
"""Run OCR on all images in a directory.
|
|
|
|
| 333 |
raise_on_error: Raise BatchProcessingError after processing if any
|
| 334 |
files failed. Successful results remain attached to the error.
|
| 335 |
jobs: Number of parallel workers for image preprocessing (1=sequential).
|
| 336 |
+
inference_concurrency: Concurrent remote inference requests. Values
|
| 337 |
+
above 1 are supported only by the vLLM backend.
|
| 338 |
memory_reset_interval: Reset the reported Metal peak-memory
|
| 339 |
counter every N successful images (0=never).
|
| 340 |
|
|
|
|
| 347 |
self._validate_run_options(task, format, profile, max_tokens)
|
| 348 |
if not isinstance(jobs, int) or isinstance(jobs, bool) or jobs < 1:
|
| 349 |
raise ValueError("jobs must be a positive integer")
|
| 350 |
+
if (
|
| 351 |
+
not isinstance(inference_concurrency, int)
|
| 352 |
+
or isinstance(inference_concurrency, bool)
|
| 353 |
+
or inference_concurrency < 1
|
| 354 |
+
):
|
| 355 |
+
raise ValueError("inference_concurrency must be a positive integer")
|
| 356 |
+
if inference_concurrency > 1 and self.backend != "vllm":
|
| 357 |
+
raise ValueError(
|
| 358 |
+
"inference_concurrency > 1 is supported only by the vLLM backend"
|
| 359 |
+
)
|
| 360 |
if (
|
| 361 |
not isinstance(memory_reset_interval, int)
|
| 362 |
or isinstance(memory_reset_interval, bool)
|
|
|
|
| 415 |
preparation_errors[img_file.name] = exc
|
| 416 |
|
| 417 |
processed_count = 0
|
| 418 |
+
processed_count_lock = Lock()
|
| 419 |
|
| 420 |
def _process_one(img_file: Path) -> tuple[str, str]:
|
| 421 |
"""Process a single image, returns (filename, output_text)."""
|
|
|
|
| 447 |
out_path = output_path_for(img_file)
|
| 448 |
save_result(doc_result, out_path, format)
|
| 449 |
|
| 450 |
+
with processed_count_lock:
|
| 451 |
+
processed_count += 1
|
| 452 |
+
reset_memory = (
|
| 453 |
+
memory_reset_interval > 0
|
| 454 |
+
and processed_count % memory_reset_interval == 0
|
| 455 |
+
)
|
| 456 |
# Start a fresh peak-memory reporting window. This does not free
|
| 457 |
# model memory; MLX manages allocations itself.
|
| 458 |
+
if reset_memory:
|
| 459 |
self._reset_memory_stats()
|
| 460 |
|
| 461 |
return img_file.name, output
|
| 462 |
|
| 463 |
+
def _run_all(advance: Callable[[], None]) -> None:
|
| 464 |
+
if inference_concurrency > 1:
|
| 465 |
+
# Resolve readiness once before worker threads issue requests.
|
| 466 |
+
self.engine.ensure_ready()
|
| 467 |
+
with ThreadPoolExecutor(
|
| 468 |
+
max_workers=inference_concurrency
|
| 469 |
+
) as executor:
|
| 470 |
+
futures = {
|
| 471 |
+
executor.submit(_process_one, img_file): img_file
|
| 472 |
+
for img_file in image_files
|
| 473 |
+
}
|
| 474 |
+
for future in as_completed(futures):
|
| 475 |
+
img_file = futures[future]
|
| 476 |
+
try:
|
| 477 |
+
name, output = future.result()
|
| 478 |
+
results[name] = output
|
| 479 |
+
except Exception as exc:
|
| 480 |
+
errors.append((img_file.name, str(exc)))
|
| 481 |
+
advance()
|
| 482 |
+
return
|
| 483 |
+
for img_file in image_files:
|
| 484 |
+
try:
|
| 485 |
+
name, output = _process_one(img_file)
|
| 486 |
+
results[name] = output
|
| 487 |
+
except Exception as exc:
|
| 488 |
+
errors.append((img_file.name, str(exc)))
|
| 489 |
+
advance()
|
| 490 |
+
|
| 491 |
if show_progress:
|
| 492 |
with Progress(
|
| 493 |
SpinnerColumn(),
|
|
|
|
| 497 |
console=console,
|
| 498 |
) as progress:
|
| 499 |
task_id = progress.add_task("Processing images...", total=len(image_files))
|
| 500 |
+
_run_all(lambda: progress.advance(task_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
else:
|
| 502 |
+
_run_all(lambda: None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
|
| 504 |
# Report errors
|
| 505 |
if errors:
|
|
|
|
| 509 |
if raise_on_error:
|
| 510 |
raise BatchProcessingError(errors, results)
|
| 511 |
|
| 512 |
+
# Preserve deterministic input order even when remote requests finish
|
| 513 |
+
# out of order.
|
| 514 |
+
return {
|
| 515 |
+
image_file.name: results[image_file.name]
|
| 516 |
+
for image_file in image_files
|
| 517 |
+
if image_file.name in results
|
| 518 |
+
}
|
| 519 |
|
| 520 |
def _reset_memory_stats(self):
|
| 521 |
"""Reset MLX's peak-memory statistic for subsequent measurements."""
|
| 522 |
+
if getattr(self.engine, "backend_name", "mlx") != "mlx":
|
| 523 |
+
return
|
| 524 |
# Avoid importing/initializing MLX merely to reset a statistic when no
|
| 525 |
# model has been loaded (important on non-Metal and headless hosts).
|
| 526 |
if not self.engine.is_loaded():
|
|
|
|
| 619 |
)
|
| 620 |
|
| 621 |
def cleanup(self):
|
| 622 |
+
"""Clean up temporary files and release the selected backend."""
|
| 623 |
lock = getattr(self, "_tmp_lock", None)
|
| 624 |
if lock is None:
|
| 625 |
return
|
|
|
|
| 627 |
if getattr(self, "_tmp_dir", None) is not None:
|
| 628 |
self._tmp_dir.cleanup()
|
| 629 |
self._tmp_dir = None
|
| 630 |
+
engine = getattr(self, "engine", None)
|
| 631 |
+
unload = getattr(engine, "unload", None)
|
| 632 |
+
if callable(unload):
|
| 633 |
+
unload()
|
| 634 |
|
| 635 |
def _validate_run_options(
|
| 636 |
self,
|
src/unlimited_ocr/preprocessing.py
CHANGED
|
@@ -544,6 +544,11 @@ def apply_gamma(img: np.ndarray, gamma: float = 1.5) -> np.ndarray:
|
|
| 544 |
raise ValueError("gamma must be a positive finite number")
|
| 545 |
# Quantize the cache key so near-identical values share a LUT.
|
| 546 |
cache_key = round(float(gamma), 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
table = _GAMMA_LUT_CACHE.get(cache_key)
|
| 548 |
if table is None:
|
| 549 |
inv_gamma = 1.0 / cache_key
|
|
@@ -680,6 +685,11 @@ def preprocess_image(
|
|
| 680 |
img, rot_angle = auto_rotate(img)
|
| 681 |
if rot_angle:
|
| 682 |
config.applied_steps.append(f"rotate_{rot_angle}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
img = sauvola_binarize(img)
|
| 684 |
config.applied_steps.append("sauvola_binarize")
|
| 685 |
if config.target_size:
|
|
|
|
| 544 |
raise ValueError("gamma must be a positive finite number")
|
| 545 |
# Quantize the cache key so near-identical values share a LUT.
|
| 546 |
cache_key = round(float(gamma), 4)
|
| 547 |
+
# Very small positive values round to zero at the cache precision. Keep
|
| 548 |
+
# the exact value in that case so valid input cannot become a division by
|
| 549 |
+
# zero merely because of the LUT cache key.
|
| 550 |
+
if cache_key == 0:
|
| 551 |
+
cache_key = float(gamma)
|
| 552 |
table = _GAMMA_LUT_CACHE.get(cache_key)
|
| 553 |
if table is None:
|
| 554 |
inv_gamma = 1.0 / cache_key
|
|
|
|
| 685 |
img, rot_angle = auto_rotate(img)
|
| 686 |
if rot_angle:
|
| 687 |
config.applied_steps.append(f"rotate_{rot_angle}")
|
| 688 |
+
if deskew:
|
| 689 |
+
deskewed = deskew_image(img)
|
| 690 |
+
if deskewed is not img:
|
| 691 |
+
config.applied_steps.append("deskew")
|
| 692 |
+
img = deskewed
|
| 693 |
img = sauvola_binarize(img)
|
| 694 |
config.applied_steps.append("sauvola_binarize")
|
| 695 |
if config.target_size:
|
tokenizer_config.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
| 13 |
"local_files_only": false,
|
| 14 |
"model_max_length": 1000000000000000019884624838656,
|
| 15 |
"pad_token": "<|▁pad▁|>",
|
| 16 |
-
"processor_class": "
|
| 17 |
"tokenizer_class": "TokenizersBackend",
|
| 18 |
"unk_token": null,
|
| 19 |
"use_default_system_prompt": false
|
|
|
|
| 13 |
"local_files_only": false,
|
| 14 |
"model_max_length": 1000000000000000019884624838656,
|
| 15 |
"pad_token": "<|▁pad▁|>",
|
| 16 |
+
"processor_class": "UnlimitedOCRHFProcessor",
|
| 17 |
"tokenizer_class": "TokenizersBackend",
|
| 18 |
"unk_token": null,
|
| 19 |
"use_default_system_prompt": false
|