granite-docling-258M β Core ML
IBM granite-docling-258M converted to Core ML for iPhone, iPad and Mac. It turns an image of a document page into DocTags (text, layout boxes, tables, formulas, code, lists), entirely on device.
- FP16 weights, no quantization. Greedy output on the GPU is token-identical to the Hugging Face model, including all 1,759 tokens of a full arXiv sample page.
- Stateful decoder split into three 10-layer chunks so each KV cache stays small enough to load on iPhone.
- Requires iOS 18 / iPadOS 18 / macOS 15 or later (
MLState, multifunction models).
| Device | Compute units | Decode | Memory |
|---|---|---|---|
| iPhone 17 Pro | CPU + GPU | 42β49 tok/s | ~280 MB |
| M-series Mac | CPU + GPU | 48β53 tok/s | β |
Use CPU + GPU (.cpuAndGPU). The Neural Engine is slower for this decoder because the KV-state
updates stay on the CPU, and the vision encoder is less accurate there (min cosine 0.983 vs 0.9987 on the
GPU), which can flip near-tie tokens.
Files
| File | Size | Role |
|---|---|---|
GraniteDoclingVisionEncoder.mlpackage |
179 MB | SigLIP2 vision tower + pixel-shuffle connector |
GraniteDoclingDecoder_chunk0.mlpackage |
68 MB | decoder layers 0β9 |
GraniteDoclingDecoder_chunk1.mlpackage |
68 MB | decoder layers 10β19 |
GraniteDoclingDecoder_chunk2.mlpackage |
68 MB | decoder layers 20β29 |
GraniteDoclingDecoder_head.mlpackage |
110 MB | final norm + vocabulary projection |
embed_tokens_fp16.bin |
110 MB | token embedding table, [100352, 576] float16, row-major |
tokenizer.json |
6.8 MB | tokenizer (identical to the base model's) |
granite_docling_coreml.json |
<1 KB | export configuration |
*.mlmodelc (five folders) |
493 MB | the same five models, already compiled |
Either use the precompiled .mlmodelc folders directly (MLModel(contentsOf:); compiled with Xcode 27's
Core ML compiler for iOS 18+, and identical for macOS 15+), or add the .mlpackage files to an Xcode
target (Xcode compiles them to .mlmodelc) or compile them at runtime with MLModel.compileModel(at:).
Model interfaces
| Model | Inputs | Outputs |
|---|---|---|
| Vision encoder | image: 512Γ512 RGB image (normalization is inside the model) |
image_embeds [1, 64, 576] fp16 |
Each decoder chunk, function prefill |
hidden_in [1, 256, 576], causal_mask [1, 1, 256, 4096], current_pos [1] int32, rope_cos / rope_sin [1, 1, 256, 64] |
hidden_out [1, 256, 576] |
Each decoder chunk, function decode |
same with length 1 | hidden_out [1, 1, 576] |
| Head | hidden [1, 1, 576] |
logits [1, 1, 100352] fp16 |
All decoder tensors are fp16. Each chunk has one MLState, kv_cache [10, 3, 4096, 128] (K and V packed
on the last axis), shared by its prefill and decode functions: load both functions of a chunk
(MLModelConfiguration.functionName) and pass the same state object to every call. Maximum context is 4096.
Running it
- Tile the page with Lanczos resampling: resize so the longest edge is 2048 (also when upscaling), keeping aspect ratio and rounding the short edge up to even; resize again so both edges are multiples of 512; cut 512Γ512 tiles row-major, then append the whole page resized to 512Γ512 as the global view. A 1540Γ1190 page gives 4Γ4 tiles + 1 global = 17 tiles.
- Encode each tile with the vision encoder: 64 embeddings per tile.
- Build the prompt (no BOS) and tokenize it with
tokenizer.json:<|start_of_role|>user<|end_of_role|> for each tile row r, column c: <fake_token_around_image><row_{r}_col_{c}> + <image>Γ64, then "\n" after each row \n<fake_token_around_image><global-img> + <image>Γ64 + <fake_token_around_image> Convert this page to docling.<|end_of_text|>\n<|start_of_role|>assistant<|end_of_role|><image>is token 100270. 1-based row/column indices, as in the base model's processor. - Embed: look up each token's row in
embed_tokens_fp16.bin; for every<image>token use the next vision embedding instead (tile order, 64 rows per tile). - Prefill in calls of 256 positions through chunk 0 β 1 β 2 (
hidden_outof one chunk ishidden_inof the next), zero-padding the last call.current_posis the absolute position of the call's first row.causal_mask[0, 0, r, c]= 0 ifc <= current_pos + r, else β10000.rope_cos/rope_sinrowr= cos / sin of(current_pos + r) Β· ΞΈ_iwithΞΈ_i = 100000^(β2i/64),i = 0β¦31, and the 32 values repeated twice (rotate-half layout).
- Head: feed the hidden row of the last real prompt token to the head; argmax of
logitsis the next token. - Decode one token at a time with the
decodefunctions, same mask and RoPE rules. Stop at<|end_of_text|>(100257),<end_of_utterance>or</doctag>.
Greedy decoding can loop on some pages; stopping when a short token cycle repeats several times is a cheap safeguard.
Conversion notes
The decoder's first prompt token develops residual activations of ~1.2Γ10β΅ (an attention-sink "massive activation"), which overflows FP16. The conversion re-expresses RMSNorm so it stays finite in FP16 and folds the norm and scale weights into the adjacent projections; grouped-query attention is computed without repeating K/V. Per-tensor INT8 quantization breaks both models, so these weights are FP16.
License and attribution
Apache 2.0, the same as the base model. All model weights are IBM's granite-docling-258M; this repository only changes their format. Please cite and credit IBM's model when you use it.
- Downloads last month
- 57
Model tree for smdesai/granite-docling-258M-coreml
Base model
ibm-granite/granite-docling-258M