| --- |
| library_name: coreai |
| tags: |
| - coreai |
| --- |
| Core AI is Apple's on-device ML runtime in iOS 27 / macOS 27 and the successor to Core ML: PyTorch models are exported with Apple's `coreai-torch` (LLMs: `coreai.llm.export`) into `.aimodel` bundles that run on the GPU or the Neural Engine, e.g. Qwen3-8B 4-bit decodes at 94 tok/s on an M4 Max GPU, MLX 90 under the same protocol ([apple-silicon-llm-bench](https://github.com/john-rocky/apple-silicon-llm-bench), macOS 27 beta 26A5353q, 2026-06-11). |
|
|
| <!-- gen-cards:devicemark begin (managed by scripts/gen-cards + tools/devicemark_row.py β edit cards.json, not this block) --> |
| This model has no row on [DeviceMark](https://devicemark.github.io/), the on-device LLM leaderboard. |
| <!-- gen-cards:devicemark end --> |
| |
| # MinerU2.5-Pro (1.2B) β Core AI whole-page document parsing |
| |
| **On-device whole-page document structuring on Core AI.** A port of |
| [`opendatalab/MinerU2.5-Pro-2605-1.2B`](https://huggingface.co/opendatalab/MinerU2.5-Pro-2605-1.2B) |
| (**Apache-2.0**, 1.2B) β a small, SOTA-quality document parser (OmniDocBench v1.6 **95.69**). Unlike a |
| per-prompt recognizer, MinerU2.5 does the *whole page in one model*: a **layout** pass finds the |
| blocks (title / text / table / formula / figure) and their reading order, then a **recognition** pass |
| reads each region into plain text, HTML tables (`<table>β¦`), or LaTeX β assembled into structured |
| Markdown / JSON. This is the piece the zoo's per-region OCRs ([Unlimited-OCR](unlimited-ocr.md), |
| [GLM-OCR](glm-ocr.md)) leave to a separate layout detector; MinerU folds it into the same weights. |
| |
| MinerU2.5 is **stock Qwen2-VL** (`Qwen2VLForConditionalGeneration`): a Qwen2-VL ViT vision tower + a |
| Qwen2-0.5B text decoder, no custom code. This port is the shipped [Qwen3-VL](qwen3-vl.md) / |
| [GLM-OCR](glm-ocr.md) vision idiom (the `image_embeds` + rope-shift static-input hook) with a Qwen2 |
| text decode β **no deepstack, no MoE, no MLA**. The vision tower runs once; its `image_embeds` are |
| injected at the image-placeholder positions (`V + slot`, row-major over the merged grid) and the text |
| decodes on top. The host runs the model twice per page (layout prompt, then recognition prompts). |
|
|
| **β¬οΈ Converted `.aimodel` bundles: |
| [mlboydaisuke/MinerU2.5-Pro-CoreAI](https://huggingface.co/mlboydaisuke/MinerU2.5-Pro-CoreAI)** β |
| `vision/` + `decoder/` (768-token **recognition** grid, Qwen2-VL ViT fp16 + int8lin decoder, pf64 |
| chunked prefill) + `layout/` (a 1036Β² **square** recognition/detection grid for the 2-stage) + |
| `tokenizer/`. Apache-2.0. |
|
|
| ## Architecture |
|
|
| - **Vision (Qwen2-VL ViT)**: embed 1280 / 32 L / 16 heads (head 80) / patch 14 / temporal 2 / |
| spatial-merge 2, out 896. **LayerNorm** blocks, fused qkv (bias), **no q/k-norm**, non-gated |
| `fc1 β quick_gelu β fc2` MLP, and the standard Qwen2-VL `PatchMerger` |
| (`ln_q β view(mergeΒ²) β Linear β GELU β Linear`). **No deepstack, no learned pos-embed** β just |
| baked 2D-rope constants. Exported as one fp16 `.aimodel`; `N` (visual tokens) is fixed by the |
| export grid. |
| - **Decoder (Qwen2-0.5B)**: hidden 896 / 24 L / GQA 14-2 / head_dim 64 / vocab 151936, `tie_word_embeddings`. |
| Separate q/k/v **with bias**, **no q/k-norm**, standard 2-norm block, silu SwiGLU, **sectioned |
| M-RoPE `[8,12,12]`** applied split-half (Qwen standard `rotate_half` β not GLM's interleave). Driven |
| on the pipelined-engine S=1 contract: `input_ids [1,1]`, `position_ids`, static `image_embeds [N,896]` |
| + `rope_shift_start` + `rope_shift_amount`. Zero embeds + `shift_start = 1<<30` β a plain Qwen2 text |
| decoder. |
|
|
| ## Verified (M4 Max + iPhone 17 Pro, GPU, Core AI pipelined engine) |
|
|
| - **iPhone 17 Pro, on-device, whole page read correctly** β the h18p AOT bundles run through |
| `KitMineruReader` in a real app (ReadDoc): a page photo β structured text (headings, paragraphs, |
| tables) with nothing leaving the device. **~4 s/page** (warm) via **chunked prefill**: the `pf64` |
| multifunction bundle feeds the 768 image tokens in **static S=64 chunks** (the engine auto-discovers |
| the `prefill` function) then decodes S=1 β the S=1 prefill (~10 s) drops to **~2.9 s**, no token cut. |
| - **End-to-end real generation on the engine (shipped portrait 768 config): the sample page read |
| verbatim** β letterbox β CLIP-norm β non-square patchify β GPU vision `.aimodel` β `image_embeds` |
| β AOT-compiled (h16c) int8lin S=1 decoder, autoregressive greedy: title + paragraph + the **full |
| table reconstructed row-by-row** (*"Quarterly Report / On-device inference shipped across all |
| product lines this quarterβ¦ / Whisper 809M 0.18 s/token / β¦"*), matching the fp32 reference. |
| **211.7 tok/s** decode (int8lin S=1, AOT h16c GPU). Portrait vision vs HF: per-token cos min |
| **0.9975**. |
| - Torch ladder vs HF fp32: text-only + vision + full-VLM argmax **706/706 exact**, max logit diff |
| **0.0001**, the generation-driving token bit-identical. |
| - Engine gate (Mac GPU): vision `image_embeds` cos **1.0002**; AOT int8lin decoder teacher-forced over |
| 706 positions β **text region 24/24 exact**, generated token exact. |
| - **int8lin vs fp16: 13 / 706 argmax flips, all at visual-token positions (0 in the text region)** β |
| the OCR text is preserved. Greedy generation byte-identical to fp16. |
|
|
| ## Run in app β `KitMineruReader` (Mac; iPhone single-pass) |
|
|
| Both grids ride the kit's VL rope-shift runtime (`VLRuntime`). Pages are CLIP-normalized and |
| patchified (the `VLImagePreprocessor` non-square + `.stretch`/`.aspectFitPad` paths added for |
| Qwen2-VL). Two entry points: |
|
|
| ```swift |
| // Single-pass (768 grid): whole page β plain text (reading order). Runs on iPhone (chunked prefill). |
| let reader = try await KitMineruReader(catalog: "mineru2.5-pro") |
| let text = try await reader.read(imageAt: documentURL) |
| |
| // 2-stage (structured Markdown, tables as <table> HTML). Needs the layout bundle too. |
| let reader = try await KitMineruReader( |
| visionDir: rec.vision, decoderDir: rec.decoder, |
| layoutVisionDir: layout.vision, layoutDecoderDir: layout.decoder) |
| let markdown = try await reader.readStructured(imageAt: documentURL) |
| ``` |
|
|
| **2-stage** (`readStructured`, mirrors `mineru-vl-utils` `two_step_extract` + `json2md`): |
| 1. **Layout** on the page **stretched into 1036Β² square** (`VLArchitecture.mineruLayout`, 37Γ37 = 1369 |
| tokens) β `Layout Detection:` emits `<|box_start|>β¦<|ref_start|>typeβ¦` blocks. A 768 portrait grid |
| mis-detects here β the square high-res grid is required. Boxes are 0β1 of the square, so they map |
| linearly onto the original page. |
| 2. **Recognition** per region on the **768 grid** (`.mineru`): each block is cropped from the original |
| and read by type β `Table Recognition:` emits **OTSL** (`<fcel>`/`<nl>`), kept (not skipped) and |
| converted to `<table>` HTML; `Formula Recognition:` β LaTeX; else plain text. |
| 3. **json2md** joins the region contents in reading order. |
|
|
| Verified end-to-end in the ReadDoc Mac app: an 8-block page β title/paragraphs + a 5-row `<table>`, |
| byte-identical to the reference. ~11 s load (both bundles) + ~11 s/page. The single-pass path also |
| runs on iPhone; the 2-stage's 1036Β² layout grid is Mac-tier (two bundles, heavy). |
|
|
| ## Pipeline (host side) |
|
|
| ``` |
| page image β letterbox into 672Γ896 (aspect-fit + white pad) β CLIP-normalize |
| β non-square patchify [3072, 1176] (block-major, 64Γ48 patches) |
| β vision .aimodel β image_embeds [768, 896] |
| β prompt: [ <|vision_start|>, <image>Γ768, <|vision_end|>, "Text Recognition:" ] |
| (image ids β V+slot; shift_start = img_start+768, shift_amount = 768β32 = 736) |
| β decoder S=1 pipelined greedy decode β tokens β detokenize β markdown / <table>HTML / LaTeX |
| ``` |
|
|
| The full 2-stage whole-page mode (layout boxes β per-region crop β recognition β `json2md`) follows |
| [`opendatalab/mineru-vl-utils`](https://github.com/opendatalab/mineru-vl-utils) |
| (`MinerUClient.two_step_extract` + `post_process.json2md`) β prompts by type: text β |
| `"Text Recognition:"` Β· table β `"Table Recognition:"` Β· formula β `"Formula Recognition:"` Β· |
| figure β `"Image Analysis:"`. |
|
|
| ## Use / reproduce |
|
|
| - **Convert**: [`conversion/export_mineru_pipelined.py`](../conversion/export_mineru_pipelined.py) |
| (`fp16` / `int8lin` / `int8hu`; vision stays fp16). |
| - **Run (Mac)**: drive the S=1 decoder bundle on the pipelined engine with three static inputs |
| (`image_embeds` + `rope_shift_start` + `rope_shift_amount`) and `COREAI_CHUNK_THRESHOLD=1`; feed the |
| prompt with the image placeholders rewritten to `V+slot`. Large decode graphs need AOT on macOS 27 |
| (`xcrun coreai-build compile β¦ --architecture h16c --preferred-compute gpu --expect-frequent-reshapes`); |
| h18p bundles are prepared for iPhone. |
| - **Knowledge**: [`knowledge/mineru-port.md`](../knowledge/mineru-port.md). |
|
|
| ## Notes |
|
|
| - **Whole-page structuring is in the model** (layout + per-region recognition, `json2md` reading-order |
| assembly) β the value over per-region OCRs. The 2-stage orchestration is host-side (Python |
| `mineru-vl-utils` is the source of truth; a Swift host is the app-integration step). |
| - **License**: base `MinerU2.5-2509` is AGPL-3.0 β this port uses **`MinerU2.5-Pro-2605` (Apache-2.0)**. |
| - **Appropriate input**: single-page documents; layout runs on a downsampled page, recognition on |
| native-res crops (pick a larger export grid for dense small text). |
| - int4 not shipped (weight-only int4 without QAT risks a quality cliff on a 1.2B model). iPhone (h18p) |
| throughput pending device verification. |
| - *Community port β not affiliated with Apple or OpenDataLab.* |
|
|
| <!-- funnel:v1 --> |
|
|
| --- |
|
|
| **More models in this format:** [Core AI Model Zoo](https://huggingface.co/collections/mlboydaisuke/core-ai-model-zoo-6a7ff330f753e8dcae04671a) β 75 models, each with the recipe that produced it. |
|
|
| **Want a different model on-device?** [Open a request](https://github.com/john-rocky/on-device-requests) β free, open weights only; the export and its measured numbers get published publicly. |
|
|
| <!-- /funnel:v1 --> |
|
|