はじめに
ローカル環境で OCR / Document Parsing 系モデルをいくつか試したかったので、WSL2 + Docker + RTX 4090 x 4 の環境に、OCR モデルを 3 種類まとめてデプロイしました。
今回起動したのは以下の 3 モデルです。
| モデル | 用途 | GPU | Host Port |
|---|---|---|---|
| baidu/Unlimited-OCR | 長文書 OCR / Markdown 化 | GPU 0 | 8884 |
| rednote-hilab/dots.mocr | 文書・図表・レイアウト解析 | GPU 1 | 8883 |
| zai-org/GLM-OCR | 軽量・高速 OCR | GPU 2 | 8888 |
全部 vLLM の OpenAI compatible API として立てています。
ポイントは、WSL2 + Docker Desktop 環境では --gpus device=... だけでは GPU 指定が期待通り効かないことがあったため、最終的に CUDA_VISIBLE_DEVICES でモデルごとに GPU を固定したことです。
環境
今回の環境です。
| 項目 | 内容 |
|---|---|
| Host | Windows + WSL2 |
| WSL distro | Ubuntu 22.04 |
| GPU | NVIDIA GeForce RTX 4090 x 4 |
| VRAM | 24GB x 4 |
| Docker | Docker Desktop + WSL Integration |
| API server | vLLM OpenAI compatible server |
nvidia-smi では以下のように 4 枚見えています。
nvidia-smi
GPU 0 NVIDIA GeForce RTX 4090
GPU 1 NVIDIA GeForce RTX 4090
GPU 2 NVIDIA GeForce RTX 4090
GPU 3 NVIDIA GeForce RTX 4090
モデル配置
モデルは Hugging Face から事前にローカルへダウンロード済みです。
/root/HuggingFaceCache/Unlimited-OCR
/root/HuggingFaceCache/DotsMOCR
/root/HuggingFaceCache/GLM-OCR
Docker コンテナには read-only でマウントします。
-v /root/HuggingFaceCache/Unlimited-OCR:/models/Unlimited-OCR:ro
のような形です。
最終構成
今回の構成は以下です。
| Container | Model | GPU | Host Port | Container Port |
|---|---|---|---|---|
| unlimited-ocr-gpu0 | baidu/Unlimited-OCR | 0 | 8884 | 8000 |
| dots-mocr-gpu1 | rednote-hilab/dots.mocr | 1 | 8883 | 8000 |
| glm-ocr-gpu2 | zai-org/GLM-OCR | 2 | 8888 | 8000 |
それぞれコンテナ内では 8000 で起動し、ホスト側のポートだけ分けています。
GPU 指定について
最初は Docker の GPU 指定で、以下のようにしていました。
--gpus '"device=2"'
また GPU UUID 指定も試しました。
--gpus '"device=GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"'
ただし、今回の WSL2 + Docker Desktop 環境では、コンテナ内の nvidia-smi で 4 枚すべて見えてしまいました。
そのため最終的には、以下の形にしています。
--gpus all
-e CUDA_VISIBLE_DEVICES=2
つまり、Docker 側では GPU capability を全部渡し、CUDA アプリケーション側で使う GPU を絞ります。
今回の割り当ては以下です。
| モデル | CUDA_VISIBLE_DEVICES |
|---|---|
| Unlimited-OCR | 0 |
| dots.mocr | 1 |
| GLM-OCR | 2 |
1. Unlimited-OCR を GPU 0 / Port 8884 で起動
docker run -d \
--name unlimited-ocr-gpu0 \
--gpus all \
-e CUDA_VISIBLE_DEVICES=0 \
--ipc=host \
-p 8884:8000 \
-v /root/HuggingFaceCache/Unlimited-OCR:/models/Unlimited-OCR:ro \
-e HF_HOME=/root/.cache/huggingface \
-e HF_TOKEN="${HF_TOKEN:-}" \
docker.1ms.run/vllm/vllm-openai:unlimited-ocr \
/models/Unlimited-OCR \
--trust-remote-code \
--host 0.0.0.0 \
--port 8000 \
--logits_processors vllm.model_executor.models.unlimited_ocr:NGramPerReqLogitsProcessor \
--no-enable-prefix-caching \
--mm-processor-cache-gb 0 \
--gpu-memory-utilization 0.80 \
--max-model-len 32768
アクセス先は以下です。
http://127.0.0.1:8884/v1
2. dots.mocr を GPU 1 / Port 8883 で起動
docker run -d \
--name dots-mocr-gpu1 \
--gpus all \
-e CUDA_VISIBLE_DEVICES=1 \
--shm-size 32g \
--ipc=host \
-p 8883:8000 \
-v /root/HuggingFaceCache/DotsMOCR:/models/DotsMOCR:ro \
docker.1ms.run/vllm/vllm-openai:v0.17.1 \
--model /models/DotsMOCR \
--served-model-name rednote-hilab/dots.mocr \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.9 \
--chat-template-content-format string \
--trust-remote-code \
--host 0.0.0.0 \
--port 8000
アクセス先は以下です。
http://127.0.0.1:8883/v1
3. GLM-OCR を GPU 2 / Port 8888 で起動
docker run -d \
--name glm-ocr-gpu2 \
--gpus all \
-e CUDA_VISIBLE_DEVICES=2 \
--shm-size 32g \
--ipc=host \
-p 8888:8000 \
-v /root/HuggingFaceCache/GLM-OCR:/models/GLM-OCR:ro \
docker.1ms.run/vllm/vllm-openai \
--model /models/GLM-OCR \
--served-model-name glm-ocr \
--trust-remote-code \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.90 \
--max-model-len 32768 \
--speculative-config '{"method": "mtp", "num_speculative_tokens": 3}'
アクセス先は以下です。
http://127.0.0.1:8888/v1
起動確認
まずコンテナを確認します。
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}"
期待値はこんな感じです。
NAMES PORTS
unlimited-ocr-gpu0 0.0.0.0:8884->8000/tcp
dots-mocr-gpu1 0.0.0.0:8883->8000/tcp
glm-ocr-gpu2 0.0.0.0:8888->8000/tcp
ログを見る場合は以下です。
docker logs -f unlimited-ocr-gpu0
docker logs -f dots-mocr-gpu1
docker logs -f glm-ocr-gpu2
/v1/models の確認
Unlimited-OCR
curl -s http://127.0.0.1:8884/v1/models | jq
dots.mocr
curl -s http://127.0.0.1:8883/v1/models | jq
GLM-OCR
curl -s http://127.0.0.1:8888/v1/models | jq
jq がない場合はインストールします。
apt update
apt install -y jq
GPU 使用状況の確認
ホスト側で確認します。
nvidia-smi
想定は以下です。
| GPU | モデル |
|---|---|
| GPU 0 | Unlimited-OCR |
| GPU 1 | dots.mocr |
| GPU 2 | GLM-OCR |
| GPU 3 | 空き |
WSL2 + Docker Desktop 環境では、nvidia-smi の Processes 表示が少し分かりづらいことがあります。
そのため、基本的には各 GPU の Memory-Usage を見て、意図した GPU に載っているか確認します。
よく使う確認コマンド
コンテナ一覧
docker ps
ポート確認
ss -lntp | grep -E '8883|8884|8888'
ログ確認
docker logs --tail 100 unlimited-ocr-gpu0
docker logs --tail 100 dots-mocr-gpu1
docker logs --tail 100 glm-ocr-gpu2
GPU 使用状況
nvidia-smi
各モデル API の疎通確認
curl -s http://127.0.0.1:8884/v1/models | jq
curl -s http://127.0.0.1:8883/v1/models | jq
curl -s http://127.0.0.1:8888/v1/models | jq
停止・削除
docker rm -f unlimited-ocr-gpu0 dots-mocr-gpu1 glm-ocr-gpu2
ハマったところ
--gpus device=N が効かなかった
今回の環境では、以下の指定をしてもコンテナ内の nvidia-smi で 4 枚すべて見えていました。
--gpus '"device=2"'
UUID 指定でも同じでした。
--gpus '"device=GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"'
最終的には以下で回避しました。
--gpus all
-e CUDA_VISIBLE_DEVICES=2
この方法なら、vLLM / PyTorch 側では指定した GPU のみを使えます。
コンテナ内の cuda:0 はホストの GPU 0 とは限らない
CUDA_VISIBLE_DEVICES=2 とすると、コンテナ内ではホストの GPU 2 が cuda:0 として見えることがあります。
つまり、対応関係はこうなります。
Host GPU 2 -> Container cuda:0
これは正常です。
モデル名は /v1/models で確認したほうがよい
--served-model-name を指定しているモデルは分かりやすいです。
rednote-hilab/dots.mocr
glm-ocr
一方、Unlimited-OCR は起動方法によって model id が /models/Unlimited-OCR のようになる場合があります。
そのため、実際に呼び出す前に必ず以下で確認しておくと安全です。
curl -s http://127.0.0.1:8884/v1/models | jq
まとめ
WSL2 + RTX 4090 x 4 の環境で、OCR モデルを 3 種類同時に vLLM Docker として起動しました。
最終的な構成は以下です。
| モデル | GPU | Port |
|---|---|---|
| Unlimited-OCR | 0 | 8884 |
| dots.mocr | 1 | 8883 |
| GLM-OCR | 2 | 8888 |
一番重要だったのは、GPU の分散方法です。
--gpus all
-e CUDA_VISIBLE_DEVICES=N
この形にすることで、WSL2 + Docker Desktop 環境でも、モデルごとに使用 GPU を分けることができました。
ローカル OCR 環境としては、以下のように使い分けると便利です。
| モデル | 使いどころ |
|---|---|
| Unlimited-OCR | 長い PDF / Markdown 化 |
| dots.mocr | レイアウト・表・図表を含む文書解析 |
| GLM-OCR | 軽量・高速な OCR API |
クラウド OCR に出しづらい文書でも、ローカル GPU 上で処理できるのはかなり便利です。
複数 GPU がある場合は、1 モデル 1 GPU で API サーバーを分けておくと、検証も運用もしやすくなります。