{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2025-10-02-sota-ocr-with-core-ml-and-dots-ocr/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "Every year our hardware is a little more powerful, our models a little smarter for each parameter. In 2025, it is more feasible than ever to run truly competitive models on-device. [dots.ocr](https://huggingface.co/rednote-hilab/dots.ocr), a 3B parameter OCR model from RedNote, surpasses Gemini 2.5 Pro in [OmniDocBench](https://github.com/opendatalab/OmniDocBench), making OCR a truly no compromises on-device use case. Running models on-device is certainly appealing to developers: no smuggling API keys, zero cost, and no network required. However, if we want these models to run on-device, we need to be mindful of the limited compute and power budgets.\n\nEnter the Neural Engine, Apple's custom AI accelerator that has shipped with every Apple device since 2017. This accelerator is designed for high performance whilst sipping battery power. Some of our testing has found the Neural Engine to be **12x more power efficient than CPU**, and **4x more power efficient than GPU**.\n\n![Compute unit energy](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/joules_cu.png)\n\nWhilst this all sounds very appealing, unfortunately the Neural Engine is only accessible through [Core ML](https://developer.apple.com/documentation/coreml), Apple's *closed source* ML framework. Furthermore, even just converting a model from PyTorch to Core ML can present some challenges, and without a preconverted model or some knowledge of the sharp edges it can be arduous for developers. Luckily, Apple also offers [MLX](https://mlx-framework.org), a more modern and flexible ML framework that targets the GPU (not the Neural Engine), and can be used in conjunction with Core ML.\n\n![NE Header](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/NE_Header.png)\n\nIn this three part series, we will provide a *reasoning trace* of how we converted `dots.ocr` to run on-device, using a combination of `CoreML` and `MLX`. This process should be applicable to many other models, and we hope that this will help highlight the ideas and tools needed for developers looking to run their own models on-device.\n\nTo follow along, clone [the repo](https://github.com/FL33TW00D/dots.ocr.ne). You'll need `uv` and `hf` installed to run the setup command:\n\n```bash\n./boostrap.sh\n```\n\nIf you just want to skip ahead and use the converted model, you can download it [here](https://huggingface.co/FL33TW00D-HF/dots.ocr.ne).\n\n## Conversion\n\nConverting from PyTorch to CoreML is a two step process:\n\n1. Capturing your PyTorch execution graph (via [`torch.jit.trace`](https://docs.pytorch.org/docs/stable/generated/torch.jit.trace.html) or, the more modern approach of [`torch.export`](https://docs.pytorch.org/docs/stable/export.html)).\n2. Compiling this converted graph to an `.mlpackage` using [`coremltools`](https://github.com/apple/coremltools).\n\nWhilst we do have a few knobs we can tweak for step 2, most of our control is in step 1, the graph we feed to `coremltools`.\n\nFollowing the programmers litany of [`make it work, make it right, make it fast`](https://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast), we will first focus on getting the conversion working on GPU, in FLOAT32, and with static shapes. Once we have this working, we can dial down the precision and try and move to the Neural Engine.\n\n## Dots.OCR\n\nDots.OCR consists of two key components: A 1.2B parameter vision encoder trained from scratch, based on the [NaViT architecture](https://arxiv.org/pdf/2307.06304), and a Qwen2.5-1.5B backbone. We will be using `CoreML` to run the vision encoder, and `MLX` to run the LM backbone.\n\n## Step 0: Understand and simplify the model\n\nIn order to convert a model, it's best to understand the structure and function before getting started. Looking at the original vision modelling file [here](https://github.com/FL33TW00D/dots.ocr.ne/blob/12c57588a97584b1c95e811475791e298e04162d/modeling_dots_vision.py), we can see that the vision encoder is similar to the QwenVL family. Like many vision encoders, the vision encoder for `dots` works on a patch basis, in this case `14x14` patches. The `dots` vision encoder is capable of processing videos and batches of images. This gives us an opportunity to simplify by only processing a single image at a time. This approach is frequent in on-device apps, where we convert a model that provides the essential functions and iterate if we want to process multiple images.\n\nWhen kicking off the conversion process, it's best to start with a minimal viable model. This means removing any bells and whistles that are not strictly necessary for the model to function. In our case, dots has many different attention implementations available for both the vision encoder and the LM backbone. CoreML has lots of infrastructure oriented around the [`scaled_dot_product_attention` operator](https://apple.github.io/coremltools/source/coremltools.converters.mil.mil.ops.defs.html#coremltools.converters.mil.mil.ops.defs.iOS18.transformers.scaled_dot_product_attention), which they introduced in iOS 18. We can simplify the model by removing all of the other attention implementations and just focusing on simple `sdpa` ([not the memory efficient variant](https://github.com/FL33TW00D/dots.ocr.ne/commit/d51b0478aacf569146cfd6f6a7542850fe61b11c)) for now, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/2cde03299efb7b64c567a3c197ef4a0259768849).\n\nOnce we've done this, we see a scary warning message when we load the model:\n\n```bash\nSliding Window Attention is enabled but not implemented for `sdpa`; unexpected results may be encountered.\n```\n\nThe model doesn't require Sliding Window Attention to function, so we can happily move on.\n\n## Step 1: A simple harness\n\nUsing `torch.jit.trace` is still the most mature method for converting models to CoreML. We usually encapsulate this in a simple harness that allows you to modify the compute units used and the precision selected.\n\nYou can check out the initial harness [here](https://github.com/FL33TW00D/dots.ocr.ne/blob/master/convert.py). If we run the following on the original code implementation:\n\n```bash\nuv run convert.py --precision FLOAT32 --compute_units CPU_AND_GPU\n```\n\nWe should bump into the first (of many) issues.\n\n## Step 2: Bug hunting\n\nIt is rare that a model will convert first time. Often, you will need to progressively make changes further and further down the execution graph until you reach the final node.\n\nOur first issue is the following error:\n\n```bash\nERROR - converting 'outer' op (located at: 'vision_tower/rotary_pos_emb/192'):\nIn op \"matmul\", when x and y are both non-const, their dtype need to match, but got x as int32 and y as fp32\n```\n\nLuckily this error gives us quite a bit of information. We can look at the `VisionRotaryEmbedding` layer and see the following code:\n\n```python\ndef forward(self, seqlen: int) -> torch.Tensor:\n    seq = torch.arange(seqlen, device=self.inv_freq.device, dtype=self.inv_freq.dtype)\n    freqs = torch.outer(seq, self.inv_freq)\n    return freqs\n```\n\nAlthough `torch.arange` has a `dtype` argument, `coremltools` ignores this for `arange` and always outputs `int32`. We can simply add a cast after the `arange` to fix this issue, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/fb968c54d35f7331d8b36b53d69e9767daba4ad3).\n\nAfter fixing this, running the conversion again leads us to our next issue at `repeat_interleave`:\n\n```bash\nERROR - converting 'repeat_interleave' op (located at: 'vision_tower/204'):\nCannot add const [None]\n```\n\nWhilst this error is less informative, we only have a single call to `repeat_interleave` in our vision encoder:\n\n```python\ncu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum(\n    dim=0,\n    dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32,\n)\n```\n\n[`cu_seqlens`](https://github.com/Dao-AILab/flash-attention/issues/850) is used for masking variable length sequences in `flash_attention_2`. It's derived from the `grid_thw` tensor, which represents `time`, `height` and `width`. Since we are only processing a single image, we can simply remove this call, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/417512e522b595e5fbddb19c6c66f69db0ecca78).\n\nOnto the next! This time, we get a more cryptic error:\n\n```bash\nERROR - converting '_internal_op_tensor_inplace_fill_' op (located at: 'vision_tower/0/attn/301_internal_tensor_assign_1'):\n_internal_op_tensor_inplace_fill does not support dynamic index\n```\n\nThis is again due to the masking logic to handle variable length sequences. Since we are only processing a single image (not a video or batch of images), we don't really need attention masking at all! Therefore, we can just use a mask of all `True`. To prepare ourselves for the Neural Engine conversion, we also switch from using a boolean mask to a float mask of all zeros, as the Neural Engine does not support `bool` tensors [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/9a1cd2a4d9d4d0c0c96aef5265f6bd306f1d603e)\n\nWith all of this done, the model should now successfully convert to CoreML! However, when we *run* the model, we get the following error:\n\n```bash\nerror: 'mps.reshape' op the result shape is not compatible with the input shape\n```\n\nThis reshape could be in multiple places! Luckily, we can use a previous warning message to help us track down the issue:\n\n```bash\nTracerWarning: Iterating over a tensor might cause the trace to be incorrect. Passing a tensor of different shape won't change the number of iterations executed (and might lead to errors or silently give incorrect results).\n  for t, h, w in grid_thw:\n```\n\nMost ML compilers [do **not** like dynamic control flow](https://x.com/tenderizzation/status/1930437803097371080). Luckily for us, as we are only processing a single image, we can simply remove the loop and process the single `h, w` pair, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/c1049e0d53b20c30a612428a84a2b6203b3273f7).\n\nAnd there we have it! If we run the conversion again, we should see that the model successfully converts and matches the original PyTorch precision:\n\n```bash\nMax difference: 0.006000518798828125, Mean difference: 1.100682402466191e-05\n```\n\n## Step 3: Benchmarking\n\nNow that we've got the model working, let's evaluate the size and performance. The good news is the model is working, the bad news is that it's over 5GB! This is completely untenable for on device deployment! To benchmark the computation time, we can use the built in XCode tooling by calling:\n\n```\nopen DotsOCR_FLOAT32.mlpackage\n```\n\nwhich will launch the XCode inspector for the model. After clicking `+ Performance Report` and launching a report on all compute devices, you should see something like the following:\n\n![GPU Perf report](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/perf_report_gpu.png)\n\nOver a second for a single forward pass of the vision encoder! We have lots of more work.\n\nIn the second part of this series, we will work on the integration between `CoreML` and `MLX`, to run the full model on-device. In the third part, we will dive deep into the optimizations required to get this model running on the Neural Engine, including quantization and dynamic shapes.\n",
  "authors": [],
  "captured_at": "2026-10-04T12:50:30.804479567+00:00",
  "category": "ai",
  "content_html": "<p>Every year our hardware is a little more powerful, our models a little smarter for each parameter. In 2025, it is more feasible than ever to run truly competitive models on-device. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/rednote-hilab/dots.ocr\">dots.ocr</a>, a 3B parameter OCR model from RedNote, surpasses Gemini 2.5 Pro in <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/opendatalab/OmniDocBench\">OmniDocBench</a>, making OCR a truly no compromises on-device use case. Running models on-device is certainly appealing to developers: no smuggling API keys, zero cost, and no network required. However, if we want these models to run on-device, we need to be mindful of the limited compute and power budgets.</p>\n<p>Enter the Neural Engine, Apple's custom AI accelerator that has shipped with every Apple device since 2017. This accelerator is designed for high performance whilst sipping battery power. Some of our testing has found the Neural Engine to be <strong>12x more power efficient than CPU</strong>, and <strong>4x more power efficient than GPU</strong>.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/joules_cu.png\" alt=\"Compute unit energy\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Whilst this all sounds very appealing, unfortunately the Neural Engine is only accessible through <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/coreml\">Core ML</a>, Apple's <em>closed source</em> ML framework. Furthermore, even just converting a model from PyTorch to Core ML can present some challenges, and without a preconverted model or some knowledge of the sharp edges it can be arduous for developers. Luckily, Apple also offers <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mlx-framework.org\">MLX</a>, a more modern and flexible ML framework that targets the GPU (not the Neural Engine), and can be used in conjunction with Core ML.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/NE_Header.png\" alt=\"NE Header\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>In this three part series, we will provide a <em>reasoning trace</em> of how we converted <code>dots.ocr</code> to run on-device, using a combination of <code>CoreML</code> and <code>MLX</code>. This process should be applicable to many other models, and we hope that this will help highlight the ideas and tools needed for developers looking to run their own models on-device.</p>\n<p>To follow along, clone <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne\">the repo</a>. You'll need <code>uv</code> and <code>hf</code> installed to run the setup command:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">./boostrap.sh</span></span>\n</span></span></code></pre>\n<p>If you just want to skip ahead and use the converted model, you can download it <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/FL33TW00D-HF/dots.ocr.ne\">here</a>.</p>\n<h2>Conversion</h2>\n<p>Converting from PyTorch to CoreML is a two step process:</p>\n<ol>\n<li>Capturing your PyTorch execution graph (via <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.pytorch.org/docs/stable/generated/torch.jit.trace.html\"><code>torch.jit.trace</code></a> or, the more modern approach of <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.pytorch.org/docs/stable/export.html\"><code>torch.export</code></a>).</li>\n<li>Compiling this converted graph to an <code>.mlpackage</code> using <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/apple/coremltools\"><code>coremltools</code></a>.</li>\n</ol>\n<p>Whilst we do have a few knobs we can tweak for step 2, most of our control is in step 1, the graph we feed to <code>coremltools</code>.</p>\n<p>Following the programmers litany of <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast\"><code>make it work, make it right, make it fast</code></a>, we will first focus on getting the conversion working on GPU, in FLOAT32, and with static shapes. Once we have this working, we can dial down the precision and try and move to the Neural Engine.</p>\n<h2>Dots.OCR</h2>\n<p>Dots.OCR consists of two key components: A 1.2B parameter vision encoder trained from scratch, based on the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/pdf/2307.06304\">NaViT architecture</a>, and a Qwen2.5-1.5B backbone. We will be using <code>CoreML</code> to run the vision encoder, and <code>MLX</code> to run the LM backbone.</p>\n<h2>Step 0: Understand and simplify the model</h2>\n<p>In order to convert a model, it's best to understand the structure and function before getting started. Looking at the original vision modelling file <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/blob/12c57588a97584b1c95e811475791e298e04162d/modeling_dots_vision.py\">here</a>, we can see that the vision encoder is similar to the QwenVL family. Like many vision encoders, the vision encoder for <code>dots</code> works on a patch basis, in this case <code>14x14</code> patches. The <code>dots</code> vision encoder is capable of processing videos and batches of images. This gives us an opportunity to simplify by only processing a single image at a time. This approach is frequent in on-device apps, where we convert a model that provides the essential functions and iterate if we want to process multiple images.</p>\n<p>When kicking off the conversion process, it's best to start with a minimal viable model. This means removing any bells and whistles that are not strictly necessary for the model to function. In our case, dots has many different attention implementations available for both the vision encoder and the LM backbone. CoreML has lots of infrastructure oriented around the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://apple.github.io/coremltools/source/coremltools.converters.mil.mil.ops.defs.html#coremltools.converters.mil.mil.ops.defs.iOS18.transformers.scaled_dot_product_attention\"><code>scaled_dot_product_attention</code> operator</a>, which they introduced in iOS 18. We can simplify the model by removing all of the other attention implementations and just focusing on simple <code>sdpa</code> (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/commit/d51b0478aacf569146cfd6f6a7542850fe61b11c\">not the memory efficient variant</a>) for now, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/commit/2cde03299efb7b64c567a3c197ef4a0259768849\">commit here</a>.</p>\n<p>Once we've done this, we see a scary warning message when we load the model:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">Sliding</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> Window Attention is enabled but not implemented for <span class=\"syntax-meta syntax-group syntax-expansion syntax-command syntax-backticks syntax-shell\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-shell\">`</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">sdpa</span></span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-shell\">`</span></span></span><span class=\"syntax-keyword syntax-operator syntax-logical syntax-continue syntax-shell\">;</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">unexpected</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> results may be encountered.</span>\n</span></span></code></pre>\n<p>The model doesn't require Sliding Window Attention to function, so we can happily move on.</p>\n<h2>Step 1: A simple harness</h2>\n<p>Using <code>torch.jit.trace</code> is still the most mature method for converting models to CoreML. We usually encapsulate this in a simple harness that allows you to modify the compute units used and the precision selected.</p>\n<p>You can check out the initial harness <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/blob/master/convert.py\">here</a>. If we run the following on the original code implementation:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> run convert.py<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>precision</span> FLOAT32<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>compute_units</span> CPU_AND_GPU</span>\n</span></span></code></pre>\n<p>We should bump into the first (of many) issues.</p>\n<h2>Step 2: Bug hunting</h2>\n<p>It is rare that a model will convert first time. Often, you will need to progressively make changes further and further down the execution graph until you reach the final node.</p>\n<p>Our first issue is the following error:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">ERROR</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> - converting <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>outer<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span> op (located at: <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>vision_tower/rotary_pos_emb/192<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span></span><span class=\"syntax-meta syntax-function-call syntax-shell\"></span><span class=\"syntax-meta syntax-post-cmd syntax-shell\">)</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-colon syntax-shell\">:</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">In</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> op <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>matmul<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span>, when x and y are both non-const, their dtype need to match, but got x as int32 and y as fp32</span>\n</span></span></code></pre>\n<p>Luckily this error gives us quite a bit of information. We can look at the <code>VisionRotaryEmbedding</code> layer and see the following code:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-storage syntax-type syntax-function syntax-python\">def</span> <span class=\"syntax-entity syntax-name syntax-function syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">forward</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-begin syntax-python\">(</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">self</span><span class=\"syntax-punctuation syntax-separator syntax-parameters syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">seqlen</span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-annotation syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-parameter syntax-python\">:</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-support syntax-type syntax-python\">int</span></span></span><span class=\"syntax-meta syntax-function syntax-parameters syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-parameters syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function syntax-python\"> </span><span class=\"syntax-meta syntax-function syntax-annotation syntax-return syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-annotation syntax-return syntax-python\">-&gt;</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">Tensor</span></span></span><span class=\"syntax-meta syntax-function syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-function syntax-begin syntax-python\">:</span></span>\n    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">seq</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">arange</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">seqlen</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">device</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">inv_freq</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">device</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">dtype</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">inv_freq</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">dtype</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n    <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">freqs</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">outer</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">seq</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-language syntax-python\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">inv_freq</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n    <span class=\"syntax-keyword syntax-control syntax-flow syntax-return syntax-python\">return</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">freqs</span></span>\n</span></span></code></pre>\n<p>Although <code>torch.arange</code> has a <code>dtype</code> argument, <code>coremltools</code> ignores this for <code>arange</code> and always outputs <code>int32</code>. We can simply add a cast after the <code>arange</code> to fix this issue, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/commit/fb968c54d35f7331d8b36b53d69e9767daba4ad3\">commit here</a>.</p>\n<p>After fixing this, running the conversion again leads us to our next issue at <code>repeat_interleave</code>:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">ERROR</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> - converting <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>repeat_interleave<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span> op (located at: <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>vision_tower/204<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span></span><span class=\"syntax-meta syntax-function-call syntax-shell\"></span><span class=\"syntax-meta syntax-post-cmd syntax-shell\">)</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-colon syntax-shell\">:</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">Cannot</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> add const <span class=\"syntax-keyword syntax-control syntax-regexp syntax-set syntax-begin syntax-shell\">[</span>None<span class=\"syntax-keyword syntax-control syntax-regexp syntax-set syntax-end syntax-shell\">]</span></span>\n</span></span></code></pre>\n<p>Whilst this error is less informative, we only have a single call to <code>repeat_interleave</code> in our vision encoder:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">cu_seqlens</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">repeat_interleave</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">grid_thw</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span>, <span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span> <span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-python\">*</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">grid_thw</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span>, <span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">2</span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">grid_thw</span></span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-begin syntax-python\">[</span></span><span class=\"syntax-meta syntax-item-access syntax-arguments syntax-python\"><span class=\"syntax-punctuation syntax-separator syntax-slice syntax-python\">:</span>, <span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">0</span></span><span class=\"syntax-meta syntax-item-access syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-brackets syntax-end syntax-python\">]</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">cumsum</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\">\n    <span class=\"syntax-variable syntax-parameter syntax-python\">dim</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">0</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">dtype</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">grid_thw</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">dtype</span></span> <span class=\"syntax-keyword syntax-control syntax-flow syntax-if syntax-inline syntax-python\">if</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">jit</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">is_tracing</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span> <span class=\"syntax-keyword syntax-control syntax-flow syntax-else syntax-inline syntax-python\">else</span> <span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">torch</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">int32</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/Dao-AILab/flash-attention/issues/850\"><code>cu_seqlens</code></a> is used for masking variable length sequences in <code>flash_attention_2</code>. It's derived from the <code>grid_thw</code> tensor, which represents <code>time</code>, <code>height</code> and <code>width</code>. Since we are only processing a single image, we can simply remove this call, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/commit/417512e522b595e5fbddb19c6c66f69db0ecca78\">commit here</a>.</p>\n<p>Onto the next! This time, we get a more cryptic error:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">ERROR</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> - converting <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>_internal_op_tensor_inplace_fill_<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span> op (located at: <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>vision_tower/0/attn/301_internal_tensor_assign_1<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span></span><span class=\"syntax-meta syntax-function-call syntax-shell\"></span><span class=\"syntax-meta syntax-post-cmd syntax-shell\">)</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-colon syntax-shell\">:</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">_internal_op_tensor_inplace_fill</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> does not support dynamic index</span>\n</span></span></code></pre>\n<p>This is again due to the masking logic to handle variable length sequences. Since we are only processing a single image (not a video or batch of images), we don't really need attention masking at all! Therefore, we can just use a mask of all <code>True</code>. To prepare ourselves for the Neural Engine conversion, we also switch from using a boolean mask to a float mask of all zeros, as the Neural Engine does not support <code>bool</code> tensors <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/commit/9a1cd2a4d9d4d0c0c96aef5265f6bd306f1d603e\">commit here</a></p>\n<p>With all of this done, the model should now successfully convert to CoreML! However, when we <em>run</em> the model, we get the following error:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">error:</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>mps.reshape<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span> op the result shape is not compatible with the input shape</span>\n</span></span></code></pre>\n<p>This reshape could be in multiple places! Luckily, we can use a previous warning message to help us track down the issue:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">TracerWarning:</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> Iterating over a tensor might cause the trace to be incorrect. Passing a tensor of different shape won<span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>t change the number of iterations executed (and might lead to errors or silently give incorrect results).\n  for t, h, w in grid_thw:\n</span></span></span></span></code></pre>\n<p>Most ML compilers <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://x.com/tenderizzation/status/1930437803097371080\">do <strong>not</strong> like dynamic control flow</a>. Luckily for us, as we are only processing a single image, we can simply remove the loop and process the single <code>h, w</code> pair, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/FL33TW00D/dots.ocr.ne/commit/c1049e0d53b20c30a612428a84a2b6203b3273f7\">commit here</a>.</p>\n<p>And there we have it! If we run the conversion again, we should see that the model successfully converts and matches the original PyTorch precision:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">Max</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> difference: 0.006000518798828125, Mean difference: 1.100682402466191e-05</span>\n</span></span></code></pre>\n<h2>Step 3: Benchmarking</h2>\n<p>Now that we've got the model working, let's evaluate the size and performance. The good news is the model is working, the bad news is that it's over 5GB! This is completely untenable for on device deployment! To benchmark the computation time, we can use the built in XCode tooling by calling:</p>\n<pre><code><span class=\"code-snippet\">open DotsOCR_FLOAT32.mlpackage\n</span></code></pre>\n<p>which will launch the XCode inspector for the model. After clicking <code>+ Performance Report</code> and launching a report on all compute devices, you should see something like the following:</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/perf_report_gpu.png\" alt=\"GPU Perf report\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Over a second for a single forward pass of the vision encoder! We have lots of more work.</p>\n<p>In the second part of this series, we will work on the integration between <code>CoreML</code> and <code>MLX</code>, to run the full model on-device. In the third part, we will dive deep into the optimizations required to get this model running on the Neural Engine, including quantization and dynamic shapes.</p>\n",
  "content_markdown": "Every year our hardware is a little more powerful, our models a little smarter for each parameter. In 2025, it is more feasible than ever to run truly competitive models on-device. [dots.ocr](https://huggingface.co/rednote-hilab/dots.ocr), a 3B parameter OCR model from RedNote, surpasses Gemini 2.5 Pro in [OmniDocBench](https://github.com/opendatalab/OmniDocBench), making OCR a truly no compromises on-device use case. Running models on-device is certainly appealing to developers: no smuggling API keys, zero cost, and no network required. However, if we want these models to run on-device, we need to be mindful of the limited compute and power budgets.\n\nEnter the Neural Engine, Apple's custom AI accelerator that has shipped with every Apple device since 2017. This accelerator is designed for high performance whilst sipping battery power. Some of our testing has found the Neural Engine to be **12x more power efficient than CPU**, and **4x more power efficient than GPU**.\n\n![Compute unit energy](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/joules_cu.png)\n\nWhilst this all sounds very appealing, unfortunately the Neural Engine is only accessible through [Core ML](https://developer.apple.com/documentation/coreml), Apple's *closed source* ML framework. Furthermore, even just converting a model from PyTorch to Core ML can present some challenges, and without a preconverted model or some knowledge of the sharp edges it can be arduous for developers. Luckily, Apple also offers [MLX](https://mlx-framework.org), a more modern and flexible ML framework that targets the GPU (not the Neural Engine), and can be used in conjunction with Core ML.\n\n![NE Header](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/NE_Header.png)\n\nIn this three part series, we will provide a *reasoning trace* of how we converted `dots.ocr` to run on-device, using a combination of `CoreML` and `MLX`. This process should be applicable to many other models, and we hope that this will help highlight the ideas and tools needed for developers looking to run their own models on-device.\n\nTo follow along, clone [the repo](https://github.com/FL33TW00D/dots.ocr.ne). You'll need `uv` and `hf` installed to run the setup command:\n\n```bash\n./boostrap.sh\n```\n\nIf you just want to skip ahead and use the converted model, you can download it [here](https://huggingface.co/FL33TW00D-HF/dots.ocr.ne).\n\n## Conversion\n\nConverting from PyTorch to CoreML is a two step process:\n\n1. Capturing your PyTorch execution graph (via [`torch.jit.trace`](https://docs.pytorch.org/docs/stable/generated/torch.jit.trace.html) or, the more modern approach of [`torch.export`](https://docs.pytorch.org/docs/stable/export.html)).\n2. Compiling this converted graph to an `.mlpackage` using [`coremltools`](https://github.com/apple/coremltools).\n\nWhilst we do have a few knobs we can tweak for step 2, most of our control is in step 1, the graph we feed to `coremltools`.\n\nFollowing the programmers litany of [`make it work, make it right, make it fast`](https://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast), we will first focus on getting the conversion working on GPU, in FLOAT32, and with static shapes. Once we have this working, we can dial down the precision and try and move to the Neural Engine.\n\n## Dots.OCR\n\nDots.OCR consists of two key components: A 1.2B parameter vision encoder trained from scratch, based on the [NaViT architecture](https://arxiv.org/pdf/2307.06304), and a Qwen2.5-1.5B backbone. We will be using `CoreML` to run the vision encoder, and `MLX` to run the LM backbone.\n\n## Step 0: Understand and simplify the model\n\nIn order to convert a model, it's best to understand the structure and function before getting started. Looking at the original vision modelling file [here](https://github.com/FL33TW00D/dots.ocr.ne/blob/12c57588a97584b1c95e811475791e298e04162d/modeling_dots_vision.py), we can see that the vision encoder is similar to the QwenVL family. Like many vision encoders, the vision encoder for `dots` works on a patch basis, in this case `14x14` patches. The `dots` vision encoder is capable of processing videos and batches of images. This gives us an opportunity to simplify by only processing a single image at a time. This approach is frequent in on-device apps, where we convert a model that provides the essential functions and iterate if we want to process multiple images.\n\nWhen kicking off the conversion process, it's best to start with a minimal viable model. This means removing any bells and whistles that are not strictly necessary for the model to function. In our case, dots has many different attention implementations available for both the vision encoder and the LM backbone. CoreML has lots of infrastructure oriented around the [`scaled_dot_product_attention` operator](https://apple.github.io/coremltools/source/coremltools.converters.mil.mil.ops.defs.html#coremltools.converters.mil.mil.ops.defs.iOS18.transformers.scaled_dot_product_attention), which they introduced in iOS 18. We can simplify the model by removing all of the other attention implementations and just focusing on simple `sdpa` ([not the memory efficient variant](https://github.com/FL33TW00D/dots.ocr.ne/commit/d51b0478aacf569146cfd6f6a7542850fe61b11c)) for now, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/2cde03299efb7b64c567a3c197ef4a0259768849).\n\nOnce we've done this, we see a scary warning message when we load the model:\n\n```bash\nSliding Window Attention is enabled but not implemented for `sdpa`; unexpected results may be encountered.\n```\n\nThe model doesn't require Sliding Window Attention to function, so we can happily move on.\n\n## Step 1: A simple harness\n\nUsing `torch.jit.trace` is still the most mature method for converting models to CoreML. We usually encapsulate this in a simple harness that allows you to modify the compute units used and the precision selected.\n\nYou can check out the initial harness [here](https://github.com/FL33TW00D/dots.ocr.ne/blob/master/convert.py). If we run the following on the original code implementation:\n\n```bash\nuv run convert.py --precision FLOAT32 --compute_units CPU_AND_GPU\n```\n\nWe should bump into the first (of many) issues.\n\n## Step 2: Bug hunting\n\nIt is rare that a model will convert first time. Often, you will need to progressively make changes further and further down the execution graph until you reach the final node.\n\nOur first issue is the following error:\n\n```bash\nERROR - converting 'outer' op (located at: 'vision_tower/rotary_pos_emb/192'):\nIn op \"matmul\", when x and y are both non-const, their dtype need to match, but got x as int32 and y as fp32\n```\n\nLuckily this error gives us quite a bit of information. We can look at the `VisionRotaryEmbedding` layer and see the following code:\n\n```python\ndef forward(self, seqlen: int) -> torch.Tensor:\n    seq = torch.arange(seqlen, device=self.inv_freq.device, dtype=self.inv_freq.dtype)\n    freqs = torch.outer(seq, self.inv_freq)\n    return freqs\n```\n\nAlthough `torch.arange` has a `dtype` argument, `coremltools` ignores this for `arange` and always outputs `int32`. We can simply add a cast after the `arange` to fix this issue, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/fb968c54d35f7331d8b36b53d69e9767daba4ad3).\n\nAfter fixing this, running the conversion again leads us to our next issue at `repeat_interleave`:\n\n```bash\nERROR - converting 'repeat_interleave' op (located at: 'vision_tower/204'):\nCannot add const [None]\n```\n\nWhilst this error is less informative, we only have a single call to `repeat_interleave` in our vision encoder:\n\n```python\ncu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]).cumsum(\n    dim=0,\n    dtype=grid_thw.dtype if torch.jit.is_tracing() else torch.int32,\n)\n```\n\n[`cu_seqlens`](https://github.com/Dao-AILab/flash-attention/issues/850) is used for masking variable length sequences in `flash_attention_2`. It's derived from the `grid_thw` tensor, which represents `time`, `height` and `width`. Since we are only processing a single image, we can simply remove this call, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/417512e522b595e5fbddb19c6c66f69db0ecca78).\n\nOnto the next! This time, we get a more cryptic error:\n\n```bash\nERROR - converting '_internal_op_tensor_inplace_fill_' op (located at: 'vision_tower/0/attn/301_internal_tensor_assign_1'):\n_internal_op_tensor_inplace_fill does not support dynamic index\n```\n\nThis is again due to the masking logic to handle variable length sequences. Since we are only processing a single image (not a video or batch of images), we don't really need attention masking at all! Therefore, we can just use a mask of all `True`. To prepare ourselves for the Neural Engine conversion, we also switch from using a boolean mask to a float mask of all zeros, as the Neural Engine does not support `bool` tensors [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/9a1cd2a4d9d4d0c0c96aef5265f6bd306f1d603e)\n\nWith all of this done, the model should now successfully convert to CoreML! However, when we *run* the model, we get the following error:\n\n```bash\nerror: 'mps.reshape' op the result shape is not compatible with the input shape\n```\n\nThis reshape could be in multiple places! Luckily, we can use a previous warning message to help us track down the issue:\n\n```bash\nTracerWarning: Iterating over a tensor might cause the trace to be incorrect. Passing a tensor of different shape won't change the number of iterations executed (and might lead to errors or silently give incorrect results).\n  for t, h, w in grid_thw:\n```\n\nMost ML compilers [do **not** like dynamic control flow](https://x.com/tenderizzation/status/1930437803097371080). Luckily for us, as we are only processing a single image, we can simply remove the loop and process the single `h, w` pair, [commit here](https://github.com/FL33TW00D/dots.ocr.ne/commit/c1049e0d53b20c30a612428a84a2b6203b3273f7).\n\nAnd there we have it! If we run the conversion again, we should see that the model successfully converts and matches the original PyTorch precision:\n\n```bash\nMax difference: 0.006000518798828125, Mean difference: 1.100682402466191e-05\n```\n\n## Step 3: Benchmarking\n\nNow that we've got the model working, let's evaluate the size and performance. The good news is the model is working, the bad news is that it's over 5GB! This is completely untenable for on device deployment! To benchmark the computation time, we can use the built in XCode tooling by calling:\n\n```\nopen DotsOCR_FLOAT32.mlpackage\n```\n\nwhich will launch the XCode inspector for the model. After clicking `+ Performance Report` and launching a report on all compute devices, you should see something like the following:\n\n![GPU Perf report](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/dots-ocr-ne/perf_report_gpu.png)\n\nOver a second for a single forward pass of the vision encoder! We have lots of more work.\n\nIn the second part of this series, we will work on the integration between `CoreML` and `MLX`, to run the full model on-device. In the third part, we will dive deep into the optimizations required to get this model running on the Neural Engine, including quantization and dynamic shapes.\n",
  "dateCreated": "2026-10-04T12:50:30.804479567+00:00",
  "date_created": "2025-10-02T00:00:00+00:00",
  "date_modified": "2025-10-02T00:00:00+00:00",
  "date_published": "2025-10-02T00:00:00+00:00",
  "description": "Every year our hardware is a little more powerful, our models a little smarter for each parameter. In 2025, it is more feasible than ever to run truly competitive models on-device. dots.ocr, a 3B parameter OCR model from RedNote, surpasses…",
  "external_url": "https://huggingface.co/blog/dots-ocr-ne",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/huggingface-co/2025/10/2025-10-02-sota-ocr-with-core-ml-and-dots-ocr.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/huggingface-co/2025/10/2025-10-02-sota-ocr-with-core-ml-and-dots-ocr.md"
  },
  "id": "urn:aggr:item:9ca6196b8eca14bc9af42fc01e1ed546e1169aa5",
  "isBasedOn": {
    "@id": "https://huggingface.co/blog/dots-ocr-ne"
  },
  "mainEntity": {
    "@id": "https://huggingface.co/blog/dots-ocr-ne",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2025-10-02-sota-ocr-with-core-ml-and-dots-ocr/#webpage"
    },
    "dateModified": "2025-10-02T00:00:00+00:00",
    "datePublished": "2025-10-02T00:00:00+00:00",
    "headline": "SOTA OCR with Core ML and dots.ocr",
    "timeRequired": "PT7M",
    "url": "https://huggingface.co/blog/dots-ocr-ne",
    "wordCount": 1487
  },
  "name": "Archived snapshot: SOTA OCR with Core ML and dots.ocr",
  "preview": null,
  "reading_minutes": 7,
  "replicated_at": null,
  "source": "huggingface-co",
  "source_name": "Hugging Face - Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-04T12:50:30.804479567+00:00",
  "title": "SOTA OCR with Core ML and dots.ocr",
  "url": "https://aggr.aymericbeaumet.com/items/huggingface-co/2025-10-02-sota-ocr-with-core-ml-and-dots-ocr/",
  "word_count": 1487
}