💻

vLLMでドキュメント解析VLM dots.ocrを使ってみた on Mac

に公開

HuggingFaceで盛り上がっているLLMをローカルで動かしてみたいなぁ🙄

おっ、なんだかdots.ocrとかいうVLM(Vision Language Model)が性能良さげだぞ👀
ふむふむ・・・vLLMで動かせるって?

vLLMってなんだかよく分からないし、Mac環境だけど、いっちょやってみっか!😋

ということで、
Mac環境でvLLMを使ってdots.ocrを動かしてみました。

vLLMはLM StuidoのようにHugging FaceのLLMモデルを使える点では同じですが、
大規模なサーバー用途でGPUをゴリゴリ使って推論させるのに向いているようです。

なので、本記事の通りにMac環境でローカルに動かしても
推論速度や効率は制限されることに注意です。

※ 最後のおまけでMPS(Metal Performance Shaders)を使った推論をしています。
 Mac環境ではvLLM(CPU動作)を使うよりMPSを活用した方がかなり早く推論できます。

動作環境

MacBookPro
チップ:Apple M4
メモリ:32GB

vLLMとは?

https://docs.vllm.ai/en/stable/
vLLMは、LLM推論とサービスのための高速で使いやすいオープンソースライブラリです。

Mac/Apple Siliconにも対応しています。
ただし、今のところApple SiliconのGPUは扱えず、CPUを動かすことしかできません。
ということで、残念ながら推論速度は期待できないです。

ドキュメント解析VLM dots.ocrとは?

以下引用です👇
https://huggingface.co/rednote-hilab/dots.ocr

dots.ocrは、優れた読書順序を維持しながら、
単一の視覚言語モデル内でレイアウト検出とコンテンツ認識を統合する、
強力な多言語ドキュメントパーサーです。
コンパクトな1.7BパラメータLLM基盤にもかかわらず、
最先端の(SOTA)パフォーマンスを達成しています。
https://raw.githubusercontent.com/rednote-hilab/dots.ocr/master/assets/chart.png

インストール手順

① uvのインストール

※ uvインストール済みの方はskipして下さい。

dots.ocr公式ではuvによる仮想環境を推奨しています。
なので、まずはuvをインストールしましょう。

uvのインストール手順はこちら👇
https://docs.astral.sh/uv/getting-started/installation/

Mac環境ならこのコマンドでOK👇

curl -LsSf https://astral.sh/uv/install.sh | sh

② vLLMのインストール

次にvLLMをインストールします。

vLLMのインストール手順はこちら👇
https://docs.vllm.ai/en/stable/getting_started/installation/cpu.html#create-a-new-python-environment

Mac環境ならこのコマンドでOK👇
※ uvで仮想環境を作ってそこにvLLMをダウンロード&インストールします。
※ バージョンは、0.9.1を指定します。(dots.ocrの検証バージョンです。)

uv venv --python 3.12 --seed
source .venv/bin/activate
git clone https://github.com/vllm-project/vllm.git
cd vllm
git checkout v0.9.1
pip install -r requirements/cpu.txt
pip install -e .

色々ダダっとTerminalが動きますが、座して待ちましょう。

③ dots.ocrのインストール

次にdots.ocrをインストールします。
uv仮想環境をアクティベートした状態で、dots.ocrをgit cloneしましょう。

dots.ocrのインストール手順はこちら👇
https://huggingface.co/rednote-hilab/dots.ocr

公式だと以下のコマンドですが、

git clone https://github.com/rednote-hilab/dots.ocr.git
cd dots.ocr
pip install -e .

このままだと、Flash Attentionライブラリのインストールでエラーが発生します。

調査すると、どうやらFlash AttentionはMac/Apple Siliconでは
コンパイルできないみたいです。

対処としては、dots.ocr/requirements.txt に
Flash Attentionに対してLinux環境マーカーを付けて、
Linux/x86_64 限定に変更しましょう。

requrements.txt
- flash-attn==2.8.0.post2
+ flash-attn==2.8.0.post2; sys_platform == "linux" and platform_machine == "x86_64"

その後、pip install -e .すればOKです。
※ Flash Attentionが無くてもSDPA(Scaled Dot-Product Attentin)を使って動かせます。

無事に完了したら、次はモデルのダウンロードです。

以下のコマンドを打ってモデルをダウンロードしましょう。

python3 tools/download_model.py

④ vLLMとdots.ocrの統合作業

次に、vLLMにdots.ocrを登録します。

dots.ocrのパス設定と、
vLLMにカスタムモデルとしてdots.ocrを登録するためのコマンドを打ちます👇

export hf_model_path=./weights/DotsOCR
export PYTHONPATH=$(dirname "$hf_model_path"):$PYTHONPATH
sed -i '' '/^from vllm\.entrypoints\.cli\.main import main$/a\
from DotsOCR import modeling_dots_ocr_vllm
' "$(which vllm)"

vLLMサーバー起動前の準備

vLLMサーバー起動用コマンドをそのまま打つと色々とエラーが発生します。
なので、Mac環境用に調整を行います。

① FlashAttentionのインポートエラーが発生するので、weights/DotsOCR/config.jsonファイルを以下に書き換える

config.json
- "attn_implementation": "flash_attention_2",
+ "attn_implementation": "sdpa",

② modeling_dots_vision.pyでFlash Attentionのインポートをコメントアウトする

modeling_dots_vision.py
# from flash_attn import flash_attn_varlen_func

③ weights/DotsOCR/config.jsonでdots.ocrのMaxトークン設定を調整する

後述するテストに合わせて74896に変更します。

config.json
- "max_position_embeddings": 131072,
+ "max_position_embeddings": 74896,
- "sliding_window": 131072,
+ "sliding_window": 74896,

④ vllmのインポートエラーが発生した場合、PYTHONPATHにリポジトリルートを足す

export PYTHONPATH="/Users/<あなたの環境に合わせてね>/vllm:$PYTHONPATH"

vLLMサーバー起動

次にvLLMサーバーを起動します。
公式に書いてあるサーバー起動コマンドは以下ですが、

CUDA_VISIBLE_DEVICES=0 vllm serve ${hf_model_path} --tensor-parallel-size 1 --gpu-memory-utilization 0.95 --chat-template-content-format string --served-model-name model --trust-remote-code

このコマンドを打つと、vLLMはGPUを使う前提(CUDA Graphを使う)に対してOSプラットフォームとしてはCPUで動かす設定になっているので、不整合エラーになって起動ができません。

対処としては、cudagraph_mode を明示的に NONE にしたコマンドを打ちます。

また、自動的にCPUを使う設定になるため、dtypeがfloat16にフォールバックしてしまいます。
(dots.ocrはbfloat16の設定)

float16だとVLMの出力が崩壊してしまうので、
float32を使うように明示的にコマンドを打ちます。

※ おそらく、float16だと表現できる数値範囲が狭くて出力が不安定になっていると思われます。
※ 私の環境では”!”が連続して出力されてしまいました。

修正コマンドはこちら👇

- CUDA_VISIBLE_DEVICES=0 vllm serve ${hf_model_path} --tensor-parallel-size 1 --gpu-memory-utilization 0.95 --chat-template-content-format string --served-model-name model --trust-remote-code
+ CUDA_VISIBLE_DEVICES=0 vllm serve ${hf_model_path} --tensor-parallel-size 1 --gpu-memory-utilization 0.95 --chat-template-content-format string --served-model-name model --trust-remote-code --dtype float32 -O '{"cudagraph_mode":"NONE"}'

サーバー起動!

コマンドを打った後、以下の状態になったらサーバー起動完了です!

動作テスト

テストとして、以下の3つの画像を使います。
元々はすべて同じ画像ですが、切り取り範囲を変えて作成しています。

demo_image1_s.jpg:demo_image1.jpg(出典: dots.ocr)を編集して作成(改変あり)

demo_image1_m.jpg:demo_image1.jpg(出典: dots.ocr)を編集して作成(改変あり)

demo_image1_l.jpg:demo_image1.jpg(出典: dots.ocr)を編集して作成(改変あり)

テストコマンドは以下ですが、

python3 dots_ocr/parser.py demo/demo_image1_s.jpg #画像ファイルのパスを指定

こちらも色々と設定を変えないとまともに動かなかったので、動いた設定を記載します。

① vLLMとdots.ocrの数値表現が合わないためにエラーが発生するので、対処として、dots.ocr/weights/DotsOCR/modeling_dots_vision.pyを修正する

bf16を使わない設定に変更します。

modeling_dots_vision.py
- def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor, bf16=True) -> torch.Tensor:
+ def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor, bf16=False) -> torch.Tensor:

② dots.ocr/dots_ocr/model/inference.pyでタイムアウト設定を変更する

CPU動作だと推論が遅いためタイムアウトになりやすいです。
明示的にタイムアウトを設定します。

inference.py
-         response = client.chat.completions.create(
-             messages=messages, 
-             model=model_name, 
-             max_completion_tokens=max_completion_tokens,
-             temperature=temperature,
-             top_p=top_p
-             )
+         response = client.chat.completions.create(
+             messages=messages, 
+             model=model_name, 
+             max_completion_tokens=max_completion_tokens,
+             temperature=temperature,
+             top_p=top_p,
+             timeout=10000,
+             )

③ weights/DotsOCR/config.jsonでdots.ocrのtorch_dtype設定を変更する

vLLMサーバー起動コマンドでdtypeを明示的に指定するのでやら無くてもよいかも。

config.json
- "torch_dtype": "bfloat16",
+ "torch_dtype": "float32",

動作テスト結果

テストして得られた画像とMarkwonを示します。
また、テストコマンドを打ってから結果が出るまでの時間(生成時間)を
ざっくり測ったので参考値として記載します。


1. demo_image1_s.jpgの結果

① 生成時間
約8秒

② 出力画像

demo_image1_s.jpgの結果

② 出力Markdown

![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVYAAADwCAIAAADU/9uTAAAGXElEQVR4nO3aT2gc1R8A8Jmd2ZndNGmWlkpFEVKoYi14EaRFKdWLB4kWCkURehUUEfWg4D8kgjfFHsUUA4rUi2CqPUihUiwei400ARU9WOJiNJvdZHZnZ+ZHO+A/+J3j4fM5LMPw9j2+hzfvfb/vBcPhsCzLoiiqqhoOh1VVlWX553Oe5+PxuCzLra2t6obxeFy/r/81Go3KG+r2dT/FDfXD5uZm3edoNKqqKgD+S8KqqjY2NqIoyvO80WhkWRYEweTkZJqm4/F4Y2Oj3W4HQZCm6d+/CxMTE2EY9nq9qampwWAQRVGz2Wy321VVjUajVquV5/nm5ma/32+323EcT05OhmE4Ho+TJNnukIG/xOvr65cuXXrhhRcOHTp09OjRJEnW1tbm5+cXFxdXV1c/+OCDU6dOvf7660888cTNN9/c7XafeuqpI0eOPPLII8Ph8N133x0MBsePH//5558Hg8Gjjz568ODBJEn6/f5nn3129erVe+65Z21tbWVl5bHHHrv99tvNf/jPqarql19+aTab8/Pzf67zH3744U8//VRVVa/Xe+ihh5599tksy8qy/P777996661+v1/v6t98881nnnmmfj59+nQcx8vLy2VZvvbaa3Nzc1mW1bnDpUuXpqamlpeXR6PRdocL/NNwOOx2uwcOHPj444/rhH80Gm1ubpZlmWXZeDxeWlpKkmRxcXFtbe2NN95YW1vL87xO71999dUXX3xxMBiUZbmyshIEwblz5y5fvpwkydLSUlEUWZbleV4Uxdzc3PHjx4ui+NfowPZqhGEYx3EURVmWRVHU7/fPnj17+fLloijSNC2K4sCBAwsLC88///x77733+OOPT01NRVFUF/bC8Hopod7eX7t2LY7j/fv3Ly0t3X333TfddFMQBM1mM7zh3nvvXVxcXF5e3uZwgX9qNJvNLMs6nc758+fPnDmzsLBw8uTJwWDQaDS2traSJNna2pqdnd23b9+FCxemp6frql59FpAkyXfffXfhwoUzZ8589NFHp0+f3rdv3w8//DAzM7Nz584wDBuNRhRFW1tbrVYry7Jer/ev4YHtFZdlGYbhr7/++uSTTx47dqzdbt9///31yV+73R4MBjt27Lh48eJ999339ttvnz179uTJk/UR4I4dOxqNRqfTSZJk//79DzzwwJ49e4qi6HQ6q6urvV6v0+nU/UxMTEQ3TExMbHO4wD/FeZ5HUTS6od1uD4fDO++8s6qqbre7d+/eNE2vXLny9ddfv/TSSwcPHjxx4sRtt9129OjRem73er2ZmZnDhw9HUbS5uRkEQVEUMzMz33zzze+//7579+66ZBAEwZUrVw4dOnTLLbf8a3hgezXSNG21Wrt3756amhqPx/EN6+vrX3zxRZ7nq6urn3/+eb3yP/zwwy+//PLc3Ny1a9fSNA2CYOfOncPhsE4N6hU+SZIHH3zw2LFjX375ZZ7nYRimaXr16tXnnnvulVde2bVr1zaHC/xT3O/3v/rqq2+//fbcuXOdTqcsy42Njfn5+dnZ2R9//PHpp5++4447Jicn60phs9m8ePHiqVOnTpw40W63z58/v7KyMjs7e/jw4eFwGMfX04o0Td95553333//k08+ueuuu3777bdPP/10YWHhyJEj9Y4A+O8Iy7Lsdrv1Pd+iKMqyTJKkqqo9e/aMRqP19fU4jlut1q5du/7444/61sBgMJienq7bX99INBrT09P19cH6okGdF6yurna73SAI9u7de+utt9YtwzDc7pCBv4RFUYTh9d84jsfjcRiGURTV0zuKoiAI8jxP07Su/9cN/j6Zh8NhnRRUVRWG4Wg0ajQaVVU1m836TZZlrVar7rDRaMRx/LfRAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDg//gfEmYpunuYvL0AAAAASUVORK5CYII=)

画像は識別できてそうですが、Markdownの結果が良くなさそうです。
文字情報ではなく、なぜかdata URLが出力されました。


2. demo_image1_m.jpgの結果

① 生成時間
約1分

② 出力画像

demo_image1_m.jpgの結果

③ 出力Markdown

EXPOSURE TO MEAT AND RISK OF LYMPHOMA

2763

TABLE II - ODDS RATIO OF HODGKIN LYMPHOMA AND NON-HODGKIN LYMPHOMA FOR INDICATORS OF EXPOSURE TO MEAT

<table><thead><tr><th rowspan="2"></th><th rowspan="2">Controls</th><th colspan="3">Hodgkin lymphoma</th><th colspan="3">Non-Hodgkin lymphoma</th></tr><tr><th>Cases</th><th>OR</th><th>95% CI</th><th>Cases</th><th>OR</th><th>95% CI</th></tr></thead><tbody><tr><td>Never Exposed (reference group)</td><td>2,273</td><td>315</td><td>1.00</td><td></td><td>1,823</td><td>1.00</td><td></td></tr></tbody></table>

生成結果はいい感じです。
ページヘッダーとキャプションと表をそれぞれ認識してくれました。


3. demo_image1_l.jpgの結果

① 生成時間
約1時間弱

② 出力画像

demo_image1_l.jpgの結果

③ 出力Markdown

EXPOSURE TO MEAT AND RISK OF LYMPHOMA

2763

TABLE II - ODDS RATIO OF HODGKIN LYMPHOMA AND NON-HODGKIN LYMPHOMA FOR INDICATORS OF EXPOSURE TO MEAT

<table><thead><tr><th rowspan="2"></th><th rowspan="2">Controls</th><th colspan="3">Hodgkin lymphoma</th><th colspan="3">Non-Hodgkin lymphoma</th></tr><tr><th>Cases</th><th>OR</th><th>95% CI</th><th>Cases</th><th>OR</th><th>95% CI</th></tr></thead><tbody><tr><td>Never Exposed (reference group)</td><td>2,273</td><td>315</td><td>1.00</td><td></td><td>1,823</td><td>1.00</td><td></td></tr><tr><td>Ever Exposed</td><td>189</td><td>24</td><td>1.06</td><td>0.65-1.71</td><td>184</td><td>1.18</td><td>0.95-1.46</td></tr><tr><td>Duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>≤5 years</td><td>52</td><td>12</td><td>1.14</td><td>0.57-2.30</td><td>49</td><td>1.25</td><td>0.84-1.86</td></tr><tr><td>6-15 years</td><td>62</td><td>8</td><td>0.98</td><td>0.44-2.20</td><td>52</td><td>1.04</td><td>0.71-1.51</td></tr><tr><td>≥16 years</td><td>73</td><td>4</td><td>1.02</td><td>0.36-2.90</td><td>82</td><td>1.27</td><td>0.92-1.76</td></tr><tr><td>p-value of test for linear trend</td><td></td><td></td><td></td><td>0.90</td><td></td><td></td><td>0.13</td></tr><tr><td>Weighted duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>≤6 months</td><td>62</td><td>14</td><td>1.54</td><td>0.79-2.99</td><td>57</td><td>1.10</td><td>0.76-1.59</td></tr><tr><td>7 months to 1 year</td><td>35</td><td>3</td><td>0.60</td><td>0.17-2.13</td><td>40</td><td>1.39</td><td>0.87-2.20</td></tr><tr><td>>1 year</td><td>90</td><td>7</td><td>0.84</td><td>0.37-1.91</td><td>86</td><td>1.17</td><td>0.87-1.59</td></tr><tr><td>p-value of test for linear trend</td><td></td><td></td><td></td><td>0.75</td><td></td><td></td><td>0.13</td></tr><tr><td>Intensity of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>Low</td><td>84</td><td>11</td><td>1.05</td><td>0.52-2.12</td><td>85</td><td>1.24</td><td>0.91-1.70</td></tr><tr><td>Medium</td><td>70</td><td>10</td><td>1.19</td><td>0.56-2.49</td><td>66</td><td>1.11</td><td>0.79-1.57</td></tr><tr><td>High</td><td>35</td><td>3</td><td>0.80</td><td>0.23-2.74</td><td>32</td><td>1.14</td><td>0.70-1.85</td></tr></tbody></table>

OR, odds ratio, adjusted for age, sex, center, and cumulative exposure to pesticides; CI, confidence interval.

ページヘッダー、キャプション、表、テキストをそれぞれ認識してくれました。
ただし、結果を生成するのに1時間弱掛かってしまいました・・・。

動作テストまとめ

Mac環境でもvLLMでdots.ocrを動かすことができました!

ただ、CPUで推論している都合上、生成速度は遅く、
demo_image1_l.jpgは元の画像の3分の1程度のピクセル数にも関わらず
生成に1時間弱も掛かりました。

また、メモリも32GBなので十分とは言えず、
SWAP領域を大量に使用しながらの動作でした。

他にも以下の設定変更で推論速度が落ちてしまったと思います。
・Flash Attention → SDPA
・dfloat16 → float32

つまり、現在の私の環境と設定では実用的に使えないですね。

vLLMがMac/Apple SiliconのGPUに対応したらもう一度テストしてみたいと思います。

おまけ:MPSを使った推論(vLLMを使わない推論)

Mac環境だとvLLMを使った推論が激遅だったため、もっと早い推論方法に変更したいと思います。

MacにはApple Silicon内蔵GPUを使うための
Apple純正ランタイム:MPS(Metal Performance Shaders)があります。
https://developer.apple.com/metal/pytorch/

このMPSをPyTorchのバックエンドとして使うことで、推論の高速化を目指します。

ということで、MPSを活用した推論のスクリプトを生成AIを使って作成してみました。
最終出力は、dots_ocr/parser.pyを使った形と同じになるようにしています。

スクリプトはこちら👇

DotsOCR_usingMPS.py
"""
DotsOCR を vLLM を使わずに、PyTorch + MPS(Apple GPU) で直接推論するスクリプト。

ざっくり流れ:
1) 実行環境(MPS/CPU) と精度(dtype) を決める
2) Tokenizer, Processor, Model を同一フォルダ(モデル重み)から読み込む
3) 画像 + 会話テンプレート(画像プレースホルダを含む) で入力テンソルを作る
4) model.generate で生成 → プロンプト部分を除去してテキストを取得
5) parser.py と同じレイアウトで JSON / 画像 / Markdown を保存

注意:
- MPS は fp16 未対応の演算があるため、まずは fp32 を推奨
- apply_chat_template を優先して使い、画像プレースホルダ数とマスク数の整合を取る
"""

import argparse
import os
from typing import Tuple, Dict, Any

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, Qwen2VLProcessor
from PIL import Image
from dots_ocr.utils.layout_utils import draw_layout_on_image, post_process_cells, is_legal_bbox
from dots_ocr.utils.format_transformer import layoutjson2md
from dots_ocr.utils.image_utils import smart_resize
import json
import os

max_pixels = 1500000

def select_device() -> torch.device:
    """実行デバイスを選択する。

    - Apple Silicon で GPU(Metal) が使える → "mps"
    - それ以外 → "cpu"
    """
    if torch.backends.mps.is_available():
        return torch.device("mps")
    return torch.device("cpu")


def select_dtype(device: torch.device, prefer_fp16: bool) -> torch.dtype:
    """数値精度(dtype) を決める。

    - MPS ではまず float32 を推奨 (未対応Op回避のため)
    - 非MPS では `--fp16` 指定時に float16 を使う
    """
    # MPS の fp16 は未最適/未対応演算があるため基本 fp32。
    if device.type == "mps":
        return torch.float32
    return torch.float16 if prefer_fp16 else torch.float32


def load_components(model_dir: str, device: torch.device, dtype: torch.dtype):
    """Tokenizer / Processor / Model を読み込み、デバイスに載せる。"""
    tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
    processor = Qwen2VLProcessor.from_pretrained(model_dir, trust_remote_code=True)
    model = AutoModelForCausalLM.from_pretrained(
        model_dir,
        trust_remote_code=True,
        torch_dtype=dtype,
        low_cpu_mem_usage=True,
    )
    model.to(device)
    model.eval()

    # pad_token 未設定時の安全策 (生成の途中でパディングが必要になるため)
    if tokenizer.pad_token_id is None and tokenizer.eos_token_id is not None:
        tokenizer.pad_token = tokenizer.eos_token

    # DotsOCR の画像トークン名と整合(念のため設定)
    try:
        processor.image_token = "<|imgpad|>"
    except Exception:
        pass

    return tokenizer, processor, model


def build_default_prompt() -> str:
    """実運用向けの既定プロンプト(画像→レイアウトJSON)。

    画像からレイアウト要素の bbox / カテゴリ / テキストを JSON で返すように指示。
    """
    return (
        "Please output the layout information from the PDF image, including each layout element's bbox, "
        "its category, and the corresponding text content within the bbox.\n\n"
        "1. Bbox format: [x1, y1, x2, y2]\n\n"
        "2. Layout Categories: The possible categories are ['Caption', 'Footnote', 'Formula', 'List-item', "
        "'Page-footer', 'Page-header', 'Picture', 'Section-header', 'Table', 'Text', 'Title'].\n\n"
        "3. Text Extraction & Formatting Rules:\n"
        "    - Picture: For the 'Picture' category, the text field should be omitted.\n"
        "    - Formula: Format its text as LaTeX.\n"
        "    - Table: Format its text as HTML.\n"
        "    - All Others (Text, Title, etc.): Format their text as Markdown.\n\n"
        "4. Constraints:\n"
        "    - The output text must be the original text from the image, with no translation.\n"
        "    - All layout elements must be sorted according to human reading order.\n\n"
        "5. Final Output: The entire output must be a single JSON object."
    )


def prepare_inputs(processor: Qwen2VLProcessor, tokenizer: AutoTokenizer, prompt: str, image_path: str, device: torch.device) -> Tuple[Dict[str, Any], Image.Image]:
    """画像とプロンプトから、モデルに与えるテンソルを作る。

    Qwen2-VL 系は chat template を通すことで、画像プレースホルダやマスクの整合を自動で取ってくれる。
    うまく行かない場合は tokenizer 側のテンプレートを試し、最後の手段としてプレーンテキストにフォールバック。
    """
    image = Image.open(image_path).convert("RGB")
    # Qwen2-VL系は apply_chat_template で画像トークンの繰り返し数を自動整合させる
    conversation = [
        {
            "role": "user",
            "content": [
                {"type": "image"},
                {"type": "text", "text": prompt},
            ],
        }
    ]
    if hasattr(processor, "apply_chat_template"):
        text = processor.apply_chat_template(conversation, add_generation_prompt=True)
    elif hasattr(tokenizer, "apply_chat_template"):
        text = tokenizer.apply_chat_template(conversation, add_generation_prompt=True)
    else:
        # 最低限のフォールバック(うまくいかない場合はテンプレート必須)
        text = prompt

    # Processor に text と images を同時に渡すと、内部で pixel_values / image_grid_thw 等を組み立ててくれる
    encoded = processor(text=[text], images=[image], return_tensors="pt", max_pixels=max_pixels)
    encoded = {k: v.to(device) if torch.is_tensor(v) else v for k, v in encoded.items()}
    return encoded, image


def generate(
    model: AutoModelForCausalLM,
    tokenizer: AutoTokenizer,
    inputs: Dict[str, Any],
    max_new_tokens: int,
    temperature: float,
    top_p: float,
) -> str:
    """テキストを生成して、プロンプト部分を除いた生成文のみを返す。"""
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            temperature=temperature,
            top_p=top_p,
            do_sample=False,
            use_cache=True,
        )
    # HFのgenerateは既定で「プロンプト+生成」を返すため、入力長でスライス
    input_len = inputs.get("input_ids").shape[1] if inputs.get("input_ids") is not None else 0
    seq = outputs[0]
    if input_len > 0 and seq.shape[0] > input_len:
        seq = seq[input_len:]
    text = tokenizer.decode(seq, skip_special_tokens=True)
    return text


def main():
    """CLI エントリポイント。

    例:
        python dots.ocr/DotsOCR_usingMPS.py dots.ocr/demo/demo_image1.jpg \
            --max-new-tokens 8192 --output ./output
    保存パスは parser.py と同じ構造に揃える。
    """
    parser = argparse.ArgumentParser(description="Run DotsOCR model with PyTorch+MPS (no vLLM)")
    parser.add_argument("image", help="Path to input image")
    parser.add_argument("--model-dir", default="/Users/<あなたの環境に合わせてね>/vllm/dots.ocr/weights/DotsOCR")
    parser.add_argument("--max-new-tokens", type=int, default=8192)
    parser.add_argument("--temperature", type=float, default=0.0)
    parser.add_argument("--top-p", type=float, default=0.9)
    parser.add_argument("--fp16", action="store_true", help="Prefer float16 on non-MPS devices")
    parser.add_argument("--output", default="./output", help="Output root directory (same structure as parser.py)")
    parser.add_argument("--prompt", default=None, help="Override default prompt")
    args = parser.parse_args()

    device = select_device()  # MPS が使えれば GPU、だめなら CPU
    dtype = select_dtype(device, prefer_fp16=args.fp16)

    print(f"device={device}, dtype={dtype}")

    tokenizer, processor, model = load_components(args.model_dir, device, dtype)
    prompt = args.prompt or build_default_prompt()

    inputs, pil_image = prepare_inputs(processor, tokenizer, prompt, args.image, device)
    output_text = generate(
        model,
        tokenizer,
        inputs,
        max_new_tokens=args.max_new_tokens,
        temperature=args.temperature,
        top_p=args.top_p,
    )
    print(output_text)

    # Save outputs with the same structure as parser.py
    base = os.path.splitext(os.path.basename(args.image))[0]
    output_root = os.path.abspath(args.output)
    save_dir = os.path.join(output_root, base)
    os.makedirs(save_dir, exist_ok=True)

    # 集約 JSONL に書く 1件分のメタ情報(parser.py 相当)
    result = {"page_no": 0, "input_height": pil_image.height, "input_width": pil_image.width}

    json_ok = False
    cells = None
    try:
        cells = json.loads(output_text)
        json_ok = True
    except Exception:
        json_ok = False

    if json_ok and isinstance(cells, (list, dict)):
        # Structured layout path
        # 1) レイアウト座標を「モデル入力解像度→元画像解像度」に戻す
        try:
            print(f"[DEBUG] Parsed cells type={type(cells).__name__}, count={len(cells) if isinstance(cells, list) else 'N/A'}")
            if isinstance(cells, list) and cells:
                print(f"[DEBUG] Sample bbox before post_process: {cells[0].get('bbox', 'N/A')}, category={cells[0].get('category', 'N/A')}")
            # keep raw copy for debug overlay
            cells_raw = cells if not isinstance(cells, list) else [c.copy() for c in cells]
            ip = getattr(processor, "image_processor", None)
            min_pixels = getattr(ip, "min_pixels", 3136)
            patch_size = int(getattr(ip, "patch_size", 14))
            # processor が実際に使用した入力解像度を grid と patch/merge から厳密に復元
            input_h, input_w = None, None
            grid = inputs.get("image_grid_thw")
            if grid is not None:
                g = grid[0].tolist() if hasattr(grid, "tolist") else grid[0]
                grid_h = int(g[1]); grid_w = int(g[2])
                input_h = grid_h * patch_size
                input_w = grid_w * patch_size
            print(f"[DEBUG] input_h={input_h}, input_w={input_w}")
            if input_h and input_w and isinstance(cells, list) and cells and isinstance(cells[0], dict) and "bbox" in cells[0]:
                cells = post_process_cells(
                    pil_image,
                    cells,
                    input_w,
                    input_h,
                    min_pixels=min_pixels,
                    max_pixels=max_pixels,
                )
                print(f"[DEBUG] Sample bbox after post_process: {cells[0].get('bbox', 'N/A')}")
                print(f"[DEBUG] is_legal_bbox={is_legal_bbox(cells)}")
        except Exception:
            print("[DEBUG] post_process_cells failed; draw raw cells.")
        
        # 2) 保存処理
        json_file_path = os.path.join(save_dir, f"{base}.json")
        with open(json_file_path, "w", encoding="utf-8") as w:
            json.dump(cells, w, ensure_ascii=False)

        try:
            image_with_layout = draw_layout_on_image(pil_image, cells)
        except Exception as e:
            print(f"[DEBUG] draw_layout_on_image failed: {e}")
            image_with_layout = pil_image

        image_layout_path = os.path.join(save_dir, f"{base}.jpg")
        image_with_layout.save(image_layout_path)
        print(f"[DEBUG] Saved overlay: {image_layout_path}")

        md_content = layoutjson2md(pil_image, cells, text_key="text")
        md_file_path = os.path.join(save_dir, f"{base}.md")
        with open(md_file_path, "w", encoding="utf-8") as md_file:
            md_file.write(md_content)

        md_nohf_file_path = os.path.join(save_dir, f"{base}_nohf.md")
        with open(md_nohf_file_path, "w", encoding="utf-8") as md_file:
            md_file.write(layoutjson2md(pil_image, cells, text_key="text", no_page_hf=True))

        result.update({
            "layout_info_path": json_file_path,
            "layout_image_path": image_layout_path,
            "md_content_path": md_file_path,
            "md_content_nohf_path": md_nohf_file_path,
        })
    else:
        # Fallback: JSON として解釈できない場合 → 生成文字列をそのまま保存
        raw_json_path = os.path.join(save_dir, f"{base}.json")
        with open(raw_json_path, "w", encoding="utf-8") as w:
            json.dump(output_text, w, ensure_ascii=False)

        image_path = os.path.join(save_dir, f"{base}.jpg")
        pil_image.save(image_path)

        md_file_path = os.path.join(save_dir, f"{base}.md")
        with open(md_file_path, "w", encoding="utf-8") as md_file:
            md_file.write(output_text)

        result.update({
            "layout_info_path": raw_json_path,
            "layout_image_path": image_path,
            "md_content_path": md_file_path,
            "filtered": True,
        })

    # Write aggregate jsonl (one line) → parser.py と同じファイル名規則
    jsonl_path = os.path.join(output_root, f"{base}.jsonl")
    with open(jsonl_path, "w", encoding="utf-8") as w:
        w.write(json.dumps(result, ensure_ascii=False) + "\n")


if __name__ == "__main__":
    main()

実行コマンドはこちら👇

python DotsOCR_usingMPS.py demo/demo_image1.jpg #画像ファイルのパスを指定

おまけ:MPSで推論した結果

4. demo_image1_s.jpgの結果

画像が小さすぎてエラーになりました。
vLLMだとエラーにならないようにリサイズしているかもしれません。


5. demo_image1_m.jpgの結果

① 生成時間
約45秒
※ vLLMより早い

② 出力画像

demo_image1_m.jpgの結果

③ 出力Markdown

EXPOSURE TO MEAT AND RISK OF LYMPHOMA

2763

TABLE II - ODDS RATIO OF HODGKIN LYMPHOMA AND NON-HODGKIN LYMPHOMA FOR INDICATORS OF EXPOSURE TO MEAT

<table><thead><tr><th rowspan="2"></th><th rowspan="2">Controls</th><th colspan="3">Hodgkin lymphoma</th><th colspan="3">Non-Hodgkin lymphoma</th></tr><tr><th>Cases</th><th>OR</th><th>95% CI</th><th>Cases</th><th>OR</th><th>95% CI</th></tr></thead><tbody><tr><td>Never Exposed (reference group)</td><td>2,273</td><td>315</td><td>1.00</td><td></td><td>1,823</td><td>1.00</td><td></td></tr></tbody></table>

vLLMと同じ出力結果になりました。
生成時間は約15秒早くなりました!


6. demo_image1_l.jpgの結果

① 生成時間
約3分30秒
※ vLLMよりかなり早い

② 出力画像

demo_image1_l.jpgの結果

③ 出力Markdown

EXPOSURE TO MEAT AND RISK OF LYMPHOMA

2763

TABLE II - ODDS RATIO OF HODGKIN LYMPHOMA AND NON-HODGKIN LYMPHOMA FOR INDICATORS OF EXPOSURE TO MEAT

<table><thead><tr><th rowspan="2"></th><th rowspan="2">Controls</th><th colspan="3">Hodgkin lymphoma</th><th colspan="3">Non-Hodgkin lymphoma</th></tr><tr><th>Cases</th><th>OR</th><th>95% CI</th><th>Cases</th><th>OR</th><th>95% CI</th></tr></thead><tbody><tr><td>Never Exposed (reference group)</td><td>2,273</td><td>315</td><td>1.00</td><td></td><td>1,823</td><td>1.00</td><td></td></tr><tr><td>Ever Exposed</td><td>189</td><td>24</td><td>1.06</td><td>0.65–1.71</td><td>184</td><td>1.18</td><td>0.95–1.46</td></tr><tr><td>Duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>≤5 years</td><td>52</td><td>12</td><td>1.14</td><td>0.57–2.30</td><td>49</td><td>1.25</td><td>0.84–1.86</td></tr><tr><td>6–15 years</td><td>62</td><td>8</td><td>0.98</td><td>0.44–2.20</td><td>52</td><td>1.04</td><td>0.71–1.51</td></tr><tr><td>≥16 years</td><td>73</td><td>4</td><td>1.02</td><td>0.36–2.90</td><td>82</td><td>1.27</td><td>0.92–1.76</td></tr><tr><td>p-value of test for linear trend</td><td></td><td></td><td></td><td>0.90</td><td></td><td></td><td>0.13</td></tr><tr><td>Weighted duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>≤6 months</td><td>62</td><td>14</td><td>1.54</td><td>0.79–2.99</td><td>57</td><td>1.10</td><td>0.76–1.59</td></tr><tr><td>7 months to 1 year</td><td>35</td><td>3</td><td>0.60</td><td>0.17–2.13</td><td>40</td><td>1.39</td><td>0.87–2.20</td></tr><tr><td>>1 year</td><td>90</td><td>7</td><td>0.84</td><td>0.37–1.91</td><td>86</td><td>1.17</td><td>0.87–1.59</td></tr><tr><td>p-value of test for linear trend</td><td></td><td></td><td></td><td>0.75</td><td></td><td></td><td>0.13</td></tr><tr><td>Intensity of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>Low</td><td>84</td><td>11</td><td>1.05</td><td>0.52–2.12</td><td>85</td><td>1.24</td><td>0.91–1.70</td></tr><tr><td>Medium</td><td>70</td><td>10</td><td>1.19</td><td>0.56–2.49</td><td>66</td><td>1.11</td><td>0.79–1.57</td></tr><tr><td>High</td><td>35</td><td>3</td><td>0.80</td><td>0.23–2.74</td><td>32</td><td>1.14</td><td>0.70–1.85</td></tr></tbody></table>

OR, odds ratio, adjusted for age, sex, center, and cumulative exposure to pesticides; CI, confidence interval.

こちらもvLLMと同じ出力結果になりました。
また、生成時間はかなり早くなりました。
vLLM(CPU使用)だと約1時間弱でしたが、こちらは約3分30秒です。
GPUのすごさを感じます・・・!


7. demo_image1.jpgの結果


demo_image1.jpg:demo_image1.jpg(出典: dots.ocr)
こちらの画像はvLLMのテストで使った加工画像の元画像です。

vLLMのテストでは使いませんでしたが、
MPSを使うならドキュメントサイズでできると思い試しました。

① 生成時間
約22分

② 出力画像

demo_image1.jpgの結果

③ 出力Markdown

EXPOSURE TO MEAT AND RISK OF LYMPHOMA

2763

TABLE II - ODDS RATIO OF HODGKIN LYMPHOMA AND NON-HODGKIN LYMPHOMA FOR INDICATORS OF EXPOSURE TO MEAT

<table><thead><tr><th rowspan="2"></th><th rowspan="2">Controls</th><th colspan="3">Hodgkin lymphoma</th><th colspan="3">Non-Hodgkin lymphoma</th></tr><tr><th>Cases</th><th>OR</th><th>95% CI</th><th>Cases</th><th>OR</th><th>95% CI</th></tr></thead><tbody><tr><td>Never Exposed (reference group)</td><td>2,273</td><td>315</td><td>1.00</td><td>-</td><td>1,823</td><td>1.00</td><td>-</td></tr><tr><td>Ever Exposed</td><td>189</td><td>24</td><td>1.06</td><td>0.65-1.71</td><td>184</td><td>1.18</td><td>0.95-1.46</td></tr><tr><td>Duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>&lt;5 years</td><td>52</td><td>12</td><td>1.14</td><td>0.57-2.30</td><td>49</td><td>1.25</td><td>0.84-1.86</td></tr><tr><td>6-15 years</td><td>62</td><td>8</td><td>0.98</td><td>0.44-2.20</td><td>52</td><td>1.04</td><td>0.71-1.51</td></tr><tr><td>&ge;16 years</td><td>73</td><td>4</td><td>1.02</td><td>0.36-2.90</td><td>82</td><td>1.27</td><td>0.92-1.76</td></tr><tr><td>p-value of test for linear trend</td><td></td><td></td><td></td><td>0.90</td><td></td><td></td><td>0.13</td></tr><tr><td>Weighted duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>&lt;6 months</td><td>62</td><td>14</td><td>1.54</td><td>0.79-2.99</td><td>57</td><td>1.10</td><td>0.76-1.59</td></tr><tr><td>7 months to 1 year</td><td>35</td><td>3</td><td>0.60</td><td>0.17-2.13</td><td>40</td><td>1.39</td><td>0.87-2.20</td></tr><tr><td>&gt;1 year</td><td>90</td><td>7</td><td>0.84</td><td>0.37-1.91</td><td>86</td><td>1.17</td><td>0.87-1.59</td></tr><tr><td>p-value of test for linear trend</td><td></td><td></td><td></td><td>0.75</td><td></td><td></td><td>0.13</td></tr><tr><td>Intensity of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>Low</td><td>84</td><td>11</td><td>1.05</td><td>0.52-2.12</td><td>85</td><td>1.24</td><td>0.91-1.70</td></tr><tr><td>Medium</td><td>70</td><td>10</td><td>1.19</td><td>0.56-2.49</td><td>66</td><td>1.11</td><td>0.79-1.57</td></tr><tr><td>High</td><td>35</td><td>3</td><td>0.80</td><td>0.23-2.74</td><td>32</td><td>1.14</td><td>0.70-1.85</td></tr></tbody></table>

OR, odds ratio, adjusted for age, sex, center, and cumulative exposure to pesticides; CI, confidence interval.

TABLE III - ODDS RATIO OF ALL NON-HODGKIN LYMPHOMA FOR EXPOSURE TO SPECIFIC TYPES OF MEAT-ALL NHL

<table><thead><tr><th rowspan="2"></th><th colspan="4">Beef meat</th><th colspan="4">Chicken meat</th><th colspan="4">Pork meat</th></tr><tr><th>Cases</th><th>Controls</th><th>OR</th><th>95% CI</th><th>Cases</th><th>Controls</th><th>OR</th><th>95% CI</th><th>Cases</th><th>Controls</th><th>OR</th><th>95% CI</th></tr></thead><tbody><tr><td>Never Exposed (Ref group)</td><td>1,823</td><td>2,273</td><td>1.00</td><td></td><td>1,823</td><td>2,273</td><td>1.00</td><td></td><td>1,823</td><td>2,273</td><td>1.00</td><td></td></tr><tr><td>Ever Exposed</td><td>117</td><td>108</td><td>1.22</td><td>0.90-1.67</td><td>1,36</td><td>129</td><td>1.19</td><td>0.91-1.55</td><td>145</td><td>143</td><td>1.09</td><td>0.83-1.42</td></tr><tr><td>Duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>&lt;5 years</td><td>40</td><td>37</td><td>1.45</td><td>0.92-2.31</td><td>30</td><td>40</td><td>0.97</td><td>0.60-1.58</td><td>39</td><td>41</td><td>1.25</td><td>0.80-1.96</td></tr><tr><td>6-15 years</td><td>29</td><td>43</td><td>0.79</td><td>0.47-1.31</td><td>42</td><td>41</td><td>1.21</td><td>0.78-1.88</td><td>44</td><td>58</td><td>0.84</td><td>0.55-1.28</td></tr><tr><td>&ge;16 years</td><td>48</td><td>28</td><td>1.63</td><td>0.93-2.88</td><td>64</td><td>48</td><td>1.36</td><td>0.90-2.06</td><td>61</td><td>43</td><td>1.28</td><td>0.81-2.03</td></tr><tr><td>p-value of test for linear trend (with ref cat)</td><td></td><td></td><td>0.23</td><td></td><td></td><td></td><td>0.11</td><td></td><td></td><td></td><td>0.54</td><td></td></tr><tr><td>Intensity of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>Low</td><td>60</td><td>59</td><td>1.26</td><td>0.86-1.83</td><td>71</td><td>68</td><td>1.24</td><td>0.88-1.75</td><td>70</td><td>72</td><td>1.15</td><td>0.82-1.62</td></tr><tr><td>Medium</td><td>42</td><td>35</td><td>1.22</td><td>0.73-2.04</td><td>47</td><td>46</td><td>1.11</td><td>0.72-1.71</td><td>55</td><td>52</td><td>1.03</td><td>0.68-1.58</td></tr><tr><td>High</td><td>15</td><td>14</td><td>0.91</td><td>0.35-2.40</td><td>18</td><td>15</td><td>1.22</td><td>0.56-2.65</td><td>20</td><td>19</td><td>0.89</td><td>0.40-1.94</td></tr><tr><td>p-value of test for linear trend (with ref cat)</td><td></td><td></td><td>0.36</td><td></td><td></td><td></td><td>0.29</td><td></td><td></td><td></td><td>0.78</td><td></td></tr></tbody></table>

<table><thead><tr><th rowspan="2"></th><th colspan="4">Mutton meat</th><th colspan="4">Other meat</th></tr><tr><th>Cases</th><th>Controls</th><th>OR</th><th>95% CI</th><th>Cases</th><th>Controls</th><th>OR</th><th>95% CI</th></tr></thead><tbody><tr><td>Never Exposed (Ref group)</td><td>1,823</td><td>2,273</td><td>1.00</td><td></td><td>1,823</td><td>2,273</td><td>1.00</td><td></td></tr><tr><td>Ever Exposed</td><td>63</td><td>71</td><td>0.99</td><td>0.66-1.47</td><td>52</td><td>47</td><td>1.07</td><td>0.67-1.70</td></tr><tr><td>Duration of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>&lt;5 years</td><td>21</td><td>20</td><td>1.35</td><td>0.71-2.56</td><td>13</td><td>15</td><td>0.92</td><td>0.41-2.06</td></tr><tr><td>6-15 years</td><td>21</td><td>27</td><td>0.87</td><td>0.46-1.62</td><td>13</td><td>10</td><td>1.16</td><td>0.48-2.81</td></tr><tr><td>&ge;16 years</td><td>21</td><td>24</td><td>0.80</td><td>0.41-1.57</td><td>26</td><td>21</td><td>1.19</td><td>0.62-2.28</td></tr><tr><td>p-value of test for linear trend (with ref cat)</td><td></td><td></td><td>0.60</td><td></td><td></td><td></td><td>0.58</td><td></td></tr><tr><td>Intensity of exposure</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td>Low</td><td>31</td><td>41</td><td>0.90</td><td>0.55-1.47</td><td>24</td><td>30</td><td>0.82</td><td>0.47-1.45</td></tr><tr><td>Medium</td><td>22</td><td>21</td><td>1.11</td><td>0.57-2.14</td><td>17</td><td>8</td><td>1.97</td><td>0.80-4.90</td></tr><tr><td>High</td><td>10</td><td>9</td><td>1.29</td><td>0.42-4.00</td><td>10</td><td>9</td><td>1.09</td><td>0.34-3.51</td></tr><tr><td>p-value of test for linear trend (with ref cat)</td><td></td><td></td><td>0.80</td><td></td><td></td><td></td><td>0.57</td><td></td></tr></tbody></table>

OR, odds ratio adjusted for age, sex, center, cumulative exposure to pesticides and other types of meat; CI, confidence interval.

sponding OR of non-Hodgkin lymphoma was 1.18 (95% CI 0.95-1.46); also for this group of lymphoma, no trend was apparent for any indicator of exposure.

When the analysis on non-Hodgkin lymphoma was repeated for specific types of meat (Table III), the OR for ever exposure to beef meat was 1.22 (95% CI 0.95-1.67), and the OR for ever exposure to chicken meat was 1.19 (95% CI 0.91-1.55). The remaining meat types, namely pork meat and mutton meat, showed either no effect or small nonsignificant increases in lymphoma risk. Exposure for more than 15 years to beef meat resulted in an OR of 1.63 (95% CI 0.93-2.88). An increase in risk was also observed

for long-term exposure to chicken meat (OR = 1.36, 95% CI 0.90-2.06). The results on intensity of exposure did not suggest a difference among meat types.

In the analyses stratified by lymphoma type, whose results are reported in Table IV, an increased risk among workers exposed to beef meat was mainly apparent for diffuse large B-cell lymphoma (OR = 1.49, 95% CI 0.96-2.33), chronic lymphocytic leukemia/ small lymphocytic lymphoma (OR = 1.35, 95% CI 0.78-2.34), and multiple myeloma (OR = 1.40, 95% CI 0.67-2.94). Stronger increases in risk were observed for each of these 3 subtypes after 16 years or more of exposure (OR 2.00, 95% CI 0.87-4.61; OR

ドキュメントサイズ画像でも結果を生成することができました!
推論時間は22分と決して早くはないですが、動かせたことに感激です。

ただ、メモリ不足が顕著になってしまい、使用メモリ量が45GBまで膨れ上がり、
SWAP領域も数十GB使うことになったので、パソコンの動作が激遅になりました。

また、この出力結果を得るためにDotsOCR_usingMPS.pyのスクリプトを2点修正してます。
(上記に載せたスクリプトは修正済み)

1つは、DotsOCR_usingMPS.pyのスクリプトで画像をモデルに渡すときに、
max_pixels=1500000で元画像をリサイズ(縮小)するようにしています。

そうしないと、元画像のピクセル数に対して確保できる物理メモリ量が足りなくなり
エラーになるからです。

もう1つは、--max-new-tokensを8192に変更しています。

そうしないと、生成結果が途中で途切れてJSON解析ができなくなるので、
出力画像と出力Markdownがうまく作れなくなるからです。

おわりに

Mac環境でvLLM(CPU動作)を使ってdots.ocrを動かしてみました。

四苦八苦しつつ生成AIの力を借りてエラーを解消し、なんとか動かすことはできましたが、
Flash Attentionやdfloat16で動かせないなど制約が多く、
さらにメモリ不足でもあったため、激遅推論という結果でした😭

また、それだけで終わるのももったい無かったので、
MPSを使った推論を試してみました。

結果として、vLLM(CPU動作)より生成速度が上がり、
ドキュメントサイズ画像も試すことができました😁

ただ、今のところ私の環境と設定では実用的とは言えない推論速度なので、
今後のvLLMやLLMモデルの進化に期待です🔥


出典とライセンス

本記事のコード例・差分は dots.ocr(MIT, © 2025 rednote-hilab)に基づいています。
また vLLM(Apache-2.0)および Transformers(Apache-2.0)を使用しています。

Discussion