OCR模型在Mac Apple Silion上本地部署

Apple Silicon Mac 本地部署 OCR 模型实战笔记

适用环境:macOS + Apple Silicon(M1/M2/M3/M4)
覆盖模型:PaddleOCR-VL、GLM-OCR、DeepSeek-OCR
最后更新:基于本地实测验证


目录


0. 基础概念速查

推理方案分层

┌─────────────────────────────────────────────┐
│  客户端层:做版面分析 + 结果整合              │
│  - PaddleOCR 的 doc_parser                  │
│  - GLM-OCR 的 glmocr                        │
│  - 直接调用 VLM(无版面分析)                │
└─────────────────────────────────────────────┘
                    │ HTTP
                    ▼
┌─────────────────────────────────────────────┐
│  推理服务层:跑 VLM 模型                     │
│  - llama-server(llama.cpp,最快)           │
│  - mlx-vlm.server(MLX 框架,更新快)         │
│  - Ollama(易用,封装好)                    │
│  - 模型客户端自己加载(无 server)            │
└─────────────────────────────────────────────┘

关键原则:OCR 识别质量主要由 VLM 模型决定,推理框架不同基本不影响识别结果,只影响速度和便利性。

模型格式对照

格式 文件 适用引擎 典型精度
PyTorch safetensors model.safetensors + .py transformers / PaddlePaddle BF16 原版
MLX safetensors MLX 风格的 safetensors mlx-vlm BF16 / 4bit / 8bit
GGUF(主模型) *.gguf llama.cpp / Ollama BF16 到 Q2(多种量化)
GGUF mmproj mmproj-*.gguf*-mmproj.gguf llama.cpp / Ollama 视觉编码器部分

VLM 必备组件:视觉编码器(mmproj)+ 语言模型。在 GGUF 生态里通常是两个独立文件;在 PyTorch/MLX 里合并在一个 safetensors 里。

模型缓存位置(macOS)

工具 默认缓存目录
paddleocr ~/.paddlex/official_models/
transformers / mlx-vlm ~/.cache/huggingface/hub/
llama-server(-hf 模式) ~/.cache/llama.cpp/
Ollama ~/.ollama/models/

1. PaddleOCR-VL 部署

百度的 PaddleOCR-VL-1.5 是目前图片/PDF 转 Markdown 效果最好的开源模型之一,与 GLM-OCR、MinerU 并列第一梯队。
模型特点:0.9B 参数 VLM,SOTA 精度 94.5% @ OmniDocBench v1.5。

1.1 架构特点

  • 文档解析是两阶段 pipeline:先用 PP-DocLayoutV3 做版面分析,再对每个版块调 PaddleOCR-VL-1.5 VLM 做识别
  • 版面分析部分依赖 PaddlePaddle 框架,VLM 部分可以换不同推理引擎
  • Mac Apple Silicon 上原生 PaddlePaddle 跑 VLM 很慢(只能 CPU),推荐把 VLM 外包给 llama.cpp 或 mlx-vlm

1.2 通用前置:安装 paddleocr 客户端

不管 VLM 用哪种后端,客户端部分都需要安装:

# 1. 安装 PaddlePaddle 框架(Mac 上用 CPU 版,≥3.2.1)
python -m pip install paddlepaddle==3.3.0 \
    -i https://www.paddlepaddle.org.cn/packages/stable/cpu/

# 2. 安装 paddleocr 及其 doc-parser extras
python -m pip install -U "paddleocr[doc-parser]"

方案 A:纯本地(PaddlePaddle 跑 VLM)

一行命令,零配置,但速度慢(~99 秒/页)。适合偶尔使用。

paddleocr doc_parser \
    -i /path/to/image.png \
    --save_path ./output

首次运行会自动下载模型到 ~/.paddlex/official_models/:

  • PP-DocLayoutV3/(版面分析)
  • PaddleOCR-VL-1.5/(VLM,PyTorch 格式)

方案 B:llama-server 后端(推荐,最快)

~16 秒/页,质量最佳

准备工作

# 安装 llama.cpp
brew install llama.cpp

# 下载 GGUF 模型(两种方式)
# 方式 1:HuggingFace CLI
huggingface-cli download PaddlePaddle/PaddleOCR-VL-1.5-GGUF \
    --local-dir ~/models/PaddleOCR-VL-1.5-GGUF

# 方式 2:浏览器手动下载两个文件
# - PaddleOCR-VL-1.5.gguf (主模型)
# - PaddleOCR-VL-1.5-mmproj.gguf (视觉编码器)

启动 llama-server

llama-server \
    -m ~/models/PaddleOCR-VL-1.5-GGUF/PaddleOCR-VL-1.5.gguf \
    --mmproj ~/models/PaddleOCR-VL-1.5-GGUF/PaddleOCR-VL-1.5-mmproj.gguf \
    --port 8080 \
    --host 0.0.0.0 \
    --temp 0

⚠️ 注意:-hf PaddlePaddle/PaddleOCR-VL-1.5-GGUF 这种自动下载方式可能无法自动识别 mmproj(因为文件命名是 XXX-mmproj.gguf,不是标准的 mmproj-XXX.gguf)。推荐手动指定路径。

客户端调用

paddleocr doc_parser \
    -i /path/to/image.png \
    --vl_rec_backend llama-cpp-server \
    --vl_rec_server_url http://127.0.0.1:8080/v1 \
    --save_path ./output

方案 C:mlx-vlm-server 后端

~22 秒/页,比 llama-server 慢一些,但更贴近 MLX 生态。

准备工作

# 从 git 安装 mlx-vlm(PyPI 版可能过旧)
pip install git+https://github.com/Blaizzy/mlx-vlm.git

启动 mlx-vlm.server

# 注意:这里不需要指定模型,server 会根据第一次请求的 model 字段按需加载
mlx_vlm.server --port 8111

客户端调用

paddleocr doc_parser \
    -i /path/to/image.png \
    --vl_rec_backend mlx-vlm-server \
    --vl_rec_server_url http://localhost:8111/ \
    --vl_rec_api_model_name PaddlePaddle/PaddleOCR-VL-1.5 \
    --save_path ./output

💡 有意思的细节:--vl_rec_api_model_name 传的是 PaddlePaddle/PaddleOCR-VL-1.5(PyTorch 格式),但 mlx-vlm 会自动从 HuggingFace 下载 PyTorch 原版并在加载时即时转换成 MLX 张量。这不是 mlx-community 量化版,是原版被动态转换。

方案 D:只用 VLM 不做版面分析(最简单,但能力受限)

不依赖 PaddlePaddle,只用 transformers 调用 VLM。只能做单块识别(指定 ocr / table / formula 等任务),不支持页级文档解析。

python -m pip install "transformers>=5.0.0"
from PIL import Image
import torch
from transformers import AutoProcessor, AutoModelForImageTextToText

MODEL_PATH = "PaddlePaddle/PaddleOCR-VL-1.5"
IMAGE_PATH = "test.png"
TASK = "ocr"  # 'ocr' | 'table' | 'chart' | 'formula' | 'spotting' | 'seal'

PROMPTS = {
    "ocr": "OCR:",
    "table": "Table Recognition:",
    "formula": "Formula Recognition:",
    "chart": "Chart Recognition:",
    "spotting": "Spotting:",
    "seal": "Seal Recognition:",
}

image = Image.open(IMAGE_PATH).convert("RGB")
max_pixels = 2048 * 28 * 28 if TASK == "spotting" else 1280 * 28 * 28

DEVICE = "mps" if torch.backends.mps.is_available() else "cpu"
model = AutoModelForImageTextToText.from_pretrained(
    MODEL_PATH, torch_dtype=torch.bfloat16
).to(DEVICE).eval()
processor = AutoProcessor.from_pretrained(MODEL_PATH)

messages = [{
    "role": "user",
    "content": [
        {"type": "image", "image": image},
        {"type": "text", "text": PROMPTS[TASK]},
    ]
}]
inputs = processor.apply_chat_template(
    messages, add_generation_prompt=True, tokenize=True,
    return_dict=True, return_tensors="pt",
    images_kwargs={"size": {
        "shortest_edge": processor.image_processor.min_pixels,
        "longest_edge": max_pixels
    }},
).to(model.device)

outputs = model.generate(**inputs, max_new_tokens=512)
result = processor.decode(
    outputs[0][inputs["input_ids"].shape[-1]:-1]
)
print(result)

1.3 PaddleOCR-VL 方案速查表

方案 速度 版面分析 部署难度 推荐
A. 纯本地 PaddlePaddle ~99s/页 极易 偶尔用
B. llama-server 后端 ~16s/页 ⭐ 日常首选
C. mlx-vlm-server 后端 ~22s/页 MLX 生态
D. transformers 直接调用 单块识别

2. GLM-OCR 部署

智谱的 GLM-OCR 也达到过 SOTA 水平,同样走"版面分析 + VLM"两阶段架构,也使用 PaddlePaddle 的 PP-DocLayoutV3 做版面。

2.1 架构特点

  • GLM-OCR SDK(客户端)做版面分析
  • VLM 后端可选:云端 MaaS API本地 MLX本地 Ollama、vLLM/SGLang(需 GPU)
  • SDK 对不同后端做了适配层(api_mode 参数)

2.2 Mac 部署:两个独立虚拟环境

GLM-OCR SDK 依赖较老的 transformers,而 mlx-vlm 需要很新的 transformers,两者直接冲突。必须用两个独立 venv,通过 HTTP 通信。

# venv 1: GLM-OCR SDK 客户端
pip install git+https://github.com/zai-org/glm-ocr.git
pip install git+https://github.com/huggingface/transformers.git

# venv 2: mlx-vlm 推理服务(另一个 venv)
pip install git+https://github.com/Blaizzy/mlx-vlm.git

方案 A:mlx-vlm 后端

⚠️ 已知问题:实测 mlx-vlm==0.4.4 对 GLM-OCR 架构的视觉编码器支持有 bug,会导致模型输出无意义的"境"字。原因是图像没被正确编码(Prompt token 数异常少,仅 17 个)。
解决办法:降级到 mlx-vlm==0.3.11(模型转换时用的版本),或等待 mlx-vlm 修复。

启动 mlx-vlm server

mlx_vlm.server --trust-remote-code --port 8080

客户端 config.yaml

pipeline:
  maas:
    enabled: false
  
  ocr_api:
    api_host: localhost
    api_port: 8080
    model: mlx-community/GLM-OCR-bf16  # mlx-vlm 需要此字段
    api_path: /chat/completions         # 注意:去掉 /v1 前缀(SDK 对 mlx-vlm 的特殊处理)

运行

glmocr parse /path/to/image.png --config my_config.yaml --output ./results/

方案 B:Ollama 后端(更稳,推荐)

Ollama 官方已打包好 GLM-OCR,无需担心 mmproj 问题。

准备

# 下载 Ollama 桌面版并启动(或用命令行)
ollama pull glm-ocr:latest
ollama serve  # Ollama 桌面版会自动启动,命令行方式需要手动

⚠️ 重要坑:Ollama 默认 num_ctx=2048,对 VLM 图片太短容易截断导致输出错误。必须扩大:

cat > Modelfile <<EOF
FROM glm-ocr
PARAMETER num_ctx 16384
EOF
ollama create glm-ocr-16k -f Modelfile

客户端 config.yaml

pipeline:
  maas:
    enabled: false
  
  ocr_api:
    api_host: localhost
    api_port: 11434
    api_path: /api/generate         # Ollama 原生端点,不是 OpenAI 兼容的 /v1/...
    model: glm-ocr:latest           # 或 glm-ocr-16k(如果创建了自定义 Modelfile)
    api_mode: ollama_generate       # 关键:告诉 SDK 用 Ollama 格式而非 OpenAI 格式

运行

glmocr parse /path/to/image.png --config my_config.yaml --output ./results/

方案 C:智谱官方云 API(不用本地部署 VLM)

保留 SDK 默认 config.yaml,提供 API key 即可。适合偶尔用或对隐私不敏感的场景。

方案 D:transformers 直接调用(无版面分析)

最简单,只用 VLM 做元素级识别。

pip install git+https://github.com/huggingface/transformers.git
from transformers import AutoProcessor, AutoModelForImageTextToText
import torch

MODEL_PATH = "zai-org/GLM-OCR"

messages = [{
    "role": "user",
    "content": [
        {"type": "image", "url": "test_image.png"},
        {"type": "text", "text": "Text Recognition:"}
    ],
}]

processor = AutoProcessor.from_pretrained(MODEL_PATH)
model = AutoModelForImageTextToText.from_pretrained(
    MODEL_PATH, torch_dtype="auto", device_map="auto"
)

inputs = processor.apply_chat_template(
    messages, tokenize=True, add_generation_prompt=True,
    return_dict=True, return_tensors="pt"
).to(model.device)
inputs.pop("token_type_ids", None)

generated_ids = model.generate(**inputs, max_new_tokens=8192)
output_text = processor.decode(
    generated_ids[0][inputs["input_ids"].shape[1]:],
    skip_special_tokens=False
)
print(output_text)

2.3 GLM-OCR 方案速查表

方案 版面分析 状态 备注
A. mlx-vlm 后端 ⚠️ 0.4.4 有 bug 需降级或等修复
B. Ollama 后端 ⭐ 可用 记得设 num_ctx
C. 云 API 可用 需 API key
D. transformers 可用 单块识别

3. DeepSeek-OCR 部署

DeepSeek 发布的 OCR 专用模型。第一代各引擎都有支持,第二代官方只支持 CUDA,Mac 需用社区 MLX 版。
特点:DeepSeek-OCR 自带 grounding 能力(带坐标输出),但不自带 PP-DocLayoutV3 这种版面分析阶段

3.1 DeepSeek-OCR(第一代)

方案 A:llama.cpp

新式命令(推荐,mtmd = multimodal):

llama-mtmd-cli \
    -m ~/.cache/huggingface/hub/models--ggml-org--DeepSeek-OCR-GGUF/snapshots/*/DeepSeek-OCR-Q8_0.gguf \
    --mmproj ~/.cache/huggingface/hub/models--ggml-org--DeepSeek-OCR-GGUF/snapshots/*/mmproj-DeepSeek-OCR-f16.gguf \
    --image /path/to/image.png \
    -p "<|grounding|>Convert the document to markdown." \
    --chat-template deepseek-ocr \
    --temp 0

旧式命令(仍可用):

llama-cli \
    -m /path/to/DeepSeek-OCR-Q8_0.gguf \
    --mmproj /path/to/mmproj-DeepSeek-OCR-f16.gguf \
    --image /path/to/image.png \
    -p "<|grounding|>Convert the document to markdown." \
    --temp 0

也可以用 llama-server 方式起 HTTP 服务,和 PaddleOCR 的部署方式相同。

方案 B:Ollama

ollama pull deepseek-ocr:latest
ollama run deepseek-ocr:latest "<image>\n<|grounding|>Convert the document to markdown. /path/to/image.png"

💡 <image>\n 是 DeepSeek 的特定 prompt 语法,告诉模型图片占位。不同 VLM 的 prompt 语法各不相同。

也可以直接在 Ollama 桌面版 GUI 拖拽图片使用。

3.2 DeepSeek-OCR-2(第二代)

  • 官方仅支持 CUDA(Nvidia GPU),Apple Silicon 无法直接用官方代码
  • mlx-community 社区转换版 可以在 Mac 上跑

使用 mlx-vlm

mlx_vlm.generate \
    --model mlx-community/DeepSeek-OCR-2-bf16 \
    --max-tokens 3000 \
    --temperature 0.0 \
    --image /path/to/image.png \
    --prompt "<|grounding|>Convert the document to markdown."

3.3 后处理:清洗 DeepSeek-OCR 的输出

DeepSeek-OCR 输出会包含 prompt、坐标标记、统计信息等噪声,需要清洗才能得到纯净的 Markdown:

import subprocess
import re
from pathlib import Path

def ocr_image(image_path):
    """调用 mlx-vlm 做 OCR"""
    result = subprocess.run(
        [
            "mlx_vlm.generate",
            "--model", "mlx-community/DeepSeek-OCR-2-bf16",
            "--image", str(image_path),
            "--prompt", "<|grounding|>Convert the document to markdown.",
            "--max-tokens", "4000",
            "--temperature", "0.0",
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.DEVNULL,
        text=True
    )
    return result.stdout

def clean_ocr_output(text):
    """清洗 DeepSeek-OCR 的原始输出"""
    # 1. 删除 prompt 行及之前的所有内容
    marker = "<|grounding|>Convert the document to markdown."
    idx = text.find(marker)
    if idx != -1:
        text = text[idx + len(marker):]

    # 2. 删除统计信息("==========" 之后的所有内容)
    stats_marker = "=========="
    idx = text.find(stats_marker)
    if idx != -1:
        text = text[:idx]

    # 3. 删除坐标标记 <|det|>[[...]]<|/det|>
    text = re.sub(r'<\|det\|>\[\[.*?\]\]<\|/det\|>', '', text)

    # 4. 删除非 image 类型的 ref 标签
    text = re.sub(
        r'<\|ref\|>(?!image|figure_title|figure).*?<\|/ref\|>',
        '', text
    )

    # 5. 清理多余空行
    text = re.sub(r'\n{3,}', '\n\n', text)

    return text.strip()

# 使用
image_path = "/path/to/image.png"
output_file = Path("output/result.md")
output_file.parent.mkdir(exist_ok=True)

raw = ocr_image(image_path)
cleaned = clean_ocr_output(raw)
output_file.write_text(cleaned)
print(f"完成,输出到 {output_file}")

3.4 相关链接


4. 横向对比与选型建议

4.1 三大模型对比

维度 PaddleOCR-VL GLM-OCR DeepSeek-OCR
参数量 0.9B 0.9B ~3B(第一代)
版面分析 ✅ PP-DocLayoutV3 ✅ PP-DocLayoutV3 ❌ 无
Grounding(坐标) ✅ Spotting ✅ 原生支持
支持格式 PyTorch / MLX / GGUF PyTorch / MLX / GGUF / Ollama GGUF / MLX
Ollama 官方支持 ✅(一代)
识别质量 SOTA(OmniDocBench 94.5) SOTA 级 良好,排版恢复稍弱
部署复杂度

4.2 场景建议

追求最佳 Markdown 还原效果(含表格、公式、版面)

PaddleOCR-VL + llama-server 后端(本文方案 1.B)

追求最简单一键部署,偶尔用用

Ollama + DeepSeek-OCR 或 GLM-OCR
→ 或者 PaddleOCR 纯本地方案(零配置但慢)

需要图片坐标定位(grounding)

DeepSeek-OCR 或 PaddleOCR-VL 的 Spotting 模式

敏感数据,不能上云

→ 任何本地方案都行,选 llama-server 最稳

偶尔几张图,不想折腾

PaddleOCR 云 API(注册送 200 页,¥0.09/页超出)
→ 或 GLM-OCR 官方云 API

4.3 我的个人推荐组合

日常生产 OCR:PaddleOCR-VL + llama-server(速度最快、质量最好、版面完整)
快速测试新模型:Ollama(拖拽 GUI 最爽)
研究 / 微调:transformers + PyTorch 或 mlx-vlm + MLX


5. 常见问题与排错

5.1 VLM 输出全是"境"字或某个重复字符

原因:图像没有被正确编码成视觉 token,模型相当于"瞎猜"。

诊断:看推理日志里的 prompt token 数。正常 VLM 图片会产生几百到几千个 token,如果只有十几个,说明 mmproj 没工作。

解决:

  • 检查 mmproj 文件是否正确加载(--mmproj 参数或日志里的 has vision encoder)
  • 换不同版本的 mlx-vlm(常见:0.4.x 有回归时降到 0.3.11)
  • 换模型量化版本(bf16 / Q8 / Q4 依次尝试)

5.2 Ollama VLM 输出乱码 / 不完整

原因:默认 num_ctx=2048 对 VLM 不够用,图像 token 被截断。

解决:用 Modelfile 扩大 ctx 到 16384 或更高。

cat > Modelfile <<EOF
FROM model-name
PARAMETER num_ctx 16384
EOF
ollama create model-name-16k -f Modelfile

5.3 PaddlePaddle 在 Mac 上安装失败

原因:uv 默认索引策略严格,找不到子依赖。

解决:加 --index-strategy unsafe-best-match:

uv add paddlepaddle==3.2.1 \
    --extra-index-url https://www.paddlepaddle.org.cn/packages/stable/cpu/ \
    --index-strategy unsafe-best-match

5.4 多项目 transformers 版本冲突

现象:某个项目装了 paddleocr(依赖老 transformers),再装 mlx-vlm 或 mineru 就冲突。

解决:不同用途用不同虚拟环境,通过 HTTP 跨进程通信,不要硬塞一起。

5.5 llama-server 的 -hf 模式识别不到 mmproj

原因:自动识别依赖 mmproj-* 命名前缀。某些 repo 用 *-mmproj.gguf(中缀)命名,识别不了。

解决:手动下载后用 -m + --mmproj 显式指定路径。

5.6 OpenAI 格式 API 调用返回 input_tokens 异常少

现象:"input_tokens": 17 之类的小数字,明明传了图片。

原因:图片字段格式不匹配当前 server 的实现。不同 server 对 image_urlimageimages 等字段支持不一。

解决:

  • 先用 server 自带的 CLI 工具(llama-mtmd-climlx_vlm.generate)绕开 HTTP 层验证模型本身没问题
  • 再换 HTTP 请求的图片字段格式

5.7 Mac 上 PaddlePaddle 跑 PaddleOCR-VL 慢

原因:PaddlePaddle 在 Apple Silicon 上只有 CPU 支持,没有 MPS/Metal 加速。

解决:VLM 部分外包给 llama-server 或 mlx-vlm.server,版面分析留给 PaddlePaddle(CPU 跑够用)。


附录:命令速查

启动各种 server

# llama-server (PaddleOCR-VL)
llama-server \
    -m ~/models/PaddleOCR-VL-1.5.gguf \
    --mmproj ~/models/PaddleOCR-VL-1.5-mmproj.gguf \
    --port 8080 --host 0.0.0.0 --temp 0

# llama-server 自动下载模式(适用于标准命名的模型)
llama-server -hf ggml-org/GLM-OCR-GGUF --port 8080 --host 0.0.0.0 --temp 0

# mlx-vlm.server(模型由首次请求决定)
mlx_vlm.server --port 8111

# Ollama(启动 + 拉模型 + 运行)
ollama pull glm-ocr
ollama run glm-ocr

客户端调用

# paddleocr + 默认本地 VLM(慢)
paddleocr doc_parser -i image.png --save_path ./output

# paddleocr + llama-server
paddleocr doc_parser -i image.png \
    --vl_rec_backend llama-cpp-server \
    --vl_rec_server_url http://127.0.0.1:8080/v1 \
    --save_path ./output

# paddleocr + mlx-vlm-server
paddleocr doc_parser -i image.png \
    --vl_rec_backend mlx-vlm-server \
    --vl_rec_server_url http://localhost:8111/ \
    --vl_rec_api_model_name PaddlePaddle/PaddleOCR-VL-1.5 \
    --save_path ./output

# glmocr + 自定义 config
glmocr parse image.png --config my_config.yaml --output ./results/

# llama-mtmd-cli 单次调用(不需要 server)
llama-mtmd-cli \
    -m model.gguf --mmproj mmproj.gguf \
    --image image.png \
    -p "OCR:" --temp 0

测试 server 是否工作(curl)

# 编码图片为 base64
B64=$(base64 -i image.png)

# 调 OpenAI 兼容 API
curl http://localhost:8080/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d "{
        \"messages\": [{
            \"role\": \"user\",
            \"content\": [
                {\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/png;base64,$B64\"}},
                {\"type\": \"text\", \"text\": \"OCR:\"}
            ]
        }],
        \"temperature\": 0
    }"

查看 GGUF 内部结构(调试用)

from gguf import GGUFReader

def inspect_gguf(path):
    r = GGUFReader(path)
    groups = {'vision': [], 'mm_proj': [], 'language': [], 'other': []}
    for t in r.tensors:
        n = t.name
        if n.startswith('v.'): groups['vision'].append(n)
        elif n.startswith('mm.'): groups['mm_proj'].append(n)
        elif n.startswith(('blk.', 'token_', 'output')): groups['language'].append(n)
        else: groups['other'].append(n)
    for k, v in groups.items():
        print(f'{k:10s}: {len(v):4d} tensors')

inspect_gguf('/path/to/model.gguf')

本文档基于 Apple Silicon Mac(M4,16GB 内存)实测验证。各模型版本、工具链在快速演进,使用时请以官方最新文档为准。

posted @ 2026-04-22 19:54  RolandHe  阅读(81)  评论(0)    收藏  举报