---
title: PaddleOCR-VL-1.6-mnn
canonical_url: "https://www.modelscope.cn/models/dr3334/PaddleOCR-VL-1.6-mnn"
md_url: "https://www.modelscope.cn/models/dr3334/PaddleOCR-VL-1.6-mnn.md"
repository: dr3334/PaddleOCR-VL-1.6-mnn
chinese_name: "PaddleOCR-VL-1.6 MNN 版"
last_updated: 2026-09-21
downloads: 6
stars: 0
---

# PaddleOCR-VL-1.6-mnn

> PaddleOCR-VL-1.6-mnn - dr3334 在 ModelScope 开源的模型。PaddleOCR-VL-1.6 MNN

- **Repository**: dr3334/PaddleOCR-VL-1.6-mnn
- **Downloads**: 6
- **Stars**: 0
- **Last updated**: 2026-09-21

Source: https://www.modelscope.cn/models/dr3334/PaddleOCR-VL-1.6-mnn

---

# PaddleOCR-VL-1.6 MNN

[PaddleOCR-VL-1.6](https://www.modelscope.cn/models/PaddlePaddle/PaddleOCR-VL-1.6)（NaViT 式 SigLIP 视觉塔 +
ERNIE-4.5-0.3B decoder，约 0.9B）的 **MNN** 版本。

视觉塔与语言模型已拆成两个独立的 MNN 图（`visual.mnn` + `llm.mnn`），
可以直接用 MNN 的 `llm_demo` 跑图片识别 —— **不需要 Python、不需要 PyTorch**。

- 转换路径：MNN 官方 `transformers/llm/export`（llmexport）
- 支持任务：`OCR:` / `Table Recognition:` / `Formula Recognition:` / `Chart Recognition:` / `Spotting:` / `Seal Recognition:`
- 许可：Apache-2.0（跟随基座）

---

## ⚠️ 先读这段：运行需要打过 patch 的 MNN

**目前的模型是用「本地打了 patch 的 MNN」导出的，上游 alibaba/MNN 主线还没有合并这些修复。**

要让这个模型跑出正确结果，**必须**给 MNN 打上第 1 个 patch：

| # | 改动 | 作用 | 运行模型是否必需 |
|---|---|---|---|
| 1 | `omni.cpp` + `omni.hpp`：新增 `Omni::paddleocrVLVisionProcess`，按 `vision_type == "paddleocr_vl"` 派发 | 补 mrope 的 `addPositionIds`。**不打这个 patch，图像位置会退化、decode 位置甚至倒退，输出直接是错的** | **必需** |
| 2 | `tokenizer.py`：`export_mtok` 补 added tokens | 修 `<fcel>`/`<nl>` 等 token 解码成空串。本仓库的 `tokenizer.mtok` **已经是修好的**，只有重新导出才需要 | 仅在重导时需要 |
| 3 | `vision.py` + `model_mapper.py`：新增 `PaddleOCRVLVision` + `vision_type` | 让 llmexport 能一并导出 `visual.mnn` | 仅在重导时需要 |

> 第 1 个 patch 是**纯新增**一个模型专属分支，**不改**公共的 `defaultVisionProcess`
> （gemma3 / internvl / deepseek-vl 等都走那条），所以对其它模型零影响。

Patch 在分支上（**尚未提 PR**）：

```bash
git clone -b feature/paddleocr-vl-support https://github.com/dr3334/MNN.git
cd MNN
# 或手动 git am out/patches/000*.patch
mkdir build_omni && cd build_omni
cmake .. -DMNN_BUILD_LLM=ON -DMNN_BUILD_LLM_OMNI=ON -DMNN_LOW_MEMORY=ON
make -j$(nproc)
```

编译产物 `build_omni/llm_demo` 就是推理入口。

---

## 1. 四个配置，怎么选

| 目录 | 视觉 | decoder | 体积 | 段落 OCR | 表格 | 公式 | 逐字符一致 | decode 速度 |
|---|---|---|---|---|---|---|---|---|
| `fp16-fp16/` | fp16 | fp16 | 1.70 GB | ✅ | ✅ | ✅ | **3/3** | ~20 tok/s |
| `int8-fp16/` | int8 | fp16 | 1.31 GB | ✅ | ✅ | 差 2 token | 2/3 | ~19 tok/s |
| **`int8-int8/`** | int8 | int8 | **1.00 GB** | ✅ | ✅ | 差 2 token | **2/3** | **~80 tok/s** |
| `int8-int4/` | int8 | int4 | 0.83 GB | 缺段落空行 | ✅ | 差 3 token | 1/3 | ~125 tok/s |

> 一致性 = 与 **PyTorch fp32 基线**输出**逐字符完全相同**的任务数（3 个任务：段落 OCR / 表格 / 公式）。
> 速度 = 本机纯 CPU（4 线程）实测 decode 吞吐，同一台机器上对比。

### 推荐

- 🥇 **首选 `int8-int8/`（1.00 GB）**：体积只有 fp16 的 59%，decode **快约 4 倍**（20 → 80 tok/s），
  准确率与「只量化视觉」持平（2/3）。唯一差异是公式的 LaTeX 对齐符（`{l}` → `{c}`），**公式内容本身一致**。
- 🥈 **公式的 LaTeX 结构必须逐字符正确** → 用 `int8-fp16/`（1.31 GB）。
- 🥉 **要 100% 对齐 PyTorch / 做回归基准** → 用 `fp16-fp16/`（1.70 GB）。
- ⚠️ `int8-int4/` 只建议在**极端体积受限 + 只做表格类任务**时使用，原因见下。

### ⚠️ 关于 `int8-int4`：GLM-OCR 的配置不能照搬

`int8-int4` 用的就是 **`MNN/GLM-OCR-MNN` 那份已发布模型的量化配置**
（`quant_bit 4` / `visual_quant_bit 8` / `lm_quant_bit 4` / `quant_block 64` / `hqq` / `act_bit 16` / `embed_bit 16`）。

实测它**不适用于 PaddleOCR-VL**：decoder 降到 int4 后，段落 OCR 开始丢结构（把句内空格换成段落空行），
整体从 3/3 掉到 1/3。**请优先用 `int8-int8`。**

---

## 2. 实测结果：差异到底出在哪

### 2.1 敏感的是 decoder，不是视觉塔

这一点和直觉相反，所以做了隔离实验（视觉与 decoder 各自单独量化）：

| 视觉 | decoder | 段落 | 表格 | 公式 | 一致 | llm 权重 | visual 权重 |
|---|---|---|---|---|---|---|---|
| fp16 | fp16 | ✅ | ✅ | ✅ | **3/3** | 688 MB | 836 MB |
| **int8** | fp16 | ✅ | ✅ | 0.957 | **2/3** | 688 MB | 445 MB |
| fp16 | **int4** | 0.992 | ✅ | 0.924 | 1/3 | 194 MB | 836 MB |
| **int8** | **int4** | 0.992 | ✅ | 0.937 | 1/3 | 194 MB | 445 MB |

（数字为 `difflib` 文本相似度，`✅` = 与基线逐字符相同）

结论：

1. **表格永远正确** —— 结构化、高重复的输出最抗量化，降到 int4 也一字不差。
2. **视觉单独降到 int8 只损失公式一项**（省 391 MB，几乎免费）。
3. **decoder 单独降到 int4 连段落 OCR 都退化**（掉到 1/3）。
4. 两者叠加 = 只有 decoder 单独的结果。实测 `int8/int4` 与 `fp16/int4` 在段落和表格上**逐字节相同**，
   公式只差 1 个 token —— 说明视觉 int8 的误差被 decoder int4 的误差完全吸收了。

### 2.2 失败模式：都是「格式 / 近邻 token」级，没有内容错误

- **段落 OCR（decoder int4）**：在第 50 个 token 处，`▁Operating`（token id 74396）
  被换成 `<0x0A><0x0A>` + `Operating`（id 23 ×2 + 84193）——
  插入了段落分隔、丢了前导空格。**正文一个字都没错**，只有空行变了。
- **公式（视觉 int8 或 decoder int4）**：`{l}`（id **93928**）→ `{c}`（id **93930**）。
  这两个 id 在词表里**紧挨着**，logit 几乎相等，量化后 argmax 翻转。
  另外 `\\`（id 71268，单个 token）被换成 `\\` + `vdots`（id 80473 + 69214）。
  **两者都是合法 LaTeX，公式看起来一致**，只是对齐符/换行符不同。

### 2.3 ⚠️ 样本量警告

**只有 3 张测试图**（英文财报段落 / 表格 / 公式）。这个矩阵适合判断**相对排序**
（视觉 vs decoder 谁更敏感、哪个配置更划算），**不适合当作绝对精度指标**。
上线前请用你自己的数据集复测。

---

## 3. 导出过程（复现）

### 3.1 前置

1. MNN 打好上面 3 个 patch（`dr3334/MNN` 的 `feature/paddleocr-vl-support` 分支）
2. `PaddleOCR-VL-1.6` 的 transformers 权重
3. 一个 **transformers 兼容 shim** —— PaddleOCR-VL 的 remote code 是按 transformers 4.55 写的，
   在新版上加载会报 `ROPE_INIT_FUNCTIONS['default']` / `compute_default_rope_parameters` 之类

### 3.2 一条命令导出（fp16 基准）

```bash
cd MNN/transformers/llm/export
python llmexport.py \
  --path /path/to/PaddleOCR-VL-1.6 \
  --export mnn \
  --dst_path /path/to/out/fp16-fp16 \
  --quant_bit 16 --quant_block 64 \
  --mnnconvert /path/to/MNN/build_paddleocr/MNNConvert
```

`--quant_bit 16` = fp16。视觉精度由独立的 `--visual_quant_bit` 控制
（内部映射：`32`→fp32、`16`→`--fp16`、其它→`--weightQuantBits N`）。

### 3.3 本仓库四个配置的完整参数

```bash
# fp16-fp16（基准）
--quant_bit 16 --quant_block 64

# int8-fp16（只量化视觉）
--quant_bit 16 --quant_block 64 --visual_quant_bit 8 \
--lm_quant_bit 16 --lm_quant_block 64 --hqq --act_bit 16 --embed_bit 16

# int8-int8（推荐）
--quant_bit 8 --quant_block 64 --visual_quant_bit 8 \
--lm_quant_bit 8 --lm_quant_block 64 --hqq --act_bit 16 --embed_bit 16

# int8-int4（GLM-OCR 配置，不推荐）
--quant_bit 4 --quant_block 64 --visual_quant_bit 8 \
--lm_quant_bit 4 --lm_quant_block 64 --hqq --act_bit 16 --embed_bit 16
```

每个目录里的 `export_args.json` 完整记录了导出参数，`config_greedy.json` 是贪心解码配置。

### 3.4 视觉塔改写了什么（为什么不是原图）

上游视觉塔是**动态分辨率**的（packed patches + 双线性插值位置编码 + 2D rope），无法直接进静态图。
导出的版本是等价的**静态方图改写**：

- patch 是纯 raster order，所以 patchify = 整图 `Conv2d(k=14, s=14)` + flatten，**不需要 gather**
- 位置编码插值到 `grid × grid`、2D SigLIP rope 的 cos/sin 都**预计算成常量 buffer**
- 归一化 `x*2-1` 留在图内，C++ 侧只做 `/255`（`image_mean=[0,0,0]` 让 `(x-mean)*norm` 与 `x*norm-mean` 等价）
- projector 的 2×2 merge 用 4 个 strided slice + concat（避开 6D rearrange）；
  `gelu(approximate='tanh')` 手工展开成基础算子（两个转换器都会把 `aten::gelu` 映射成 `GELU_STANDARD`）

两个接口契约（自己重导时必踩）：输出必须是 `[n_tok, 1, hidden]` 三维
（`Omni::embedding` 要和 `Llm::embedding()` 的 `[seq,1,hidden]` 沿 axis 0 拼接），
输入名用 `input_images`。

---

## 4. 输入图片怎么处理

### 4.1 你只需要给一个图片路径

`llm_demo` 从 prompt 里的 `<img>...</img>` 读图，**预处理全部由 C++ 侧自动完成**，
不需要你在外面 resize 或归一化：

```bash
printf '<img>%s/block.png</img>Table Recognition:' "$PWD" > /tmp/p.txt
llm_demo config_greedy.json /tmp/p.txt
```

### 4.2 完整预处理链路

| # | 位置 | 做什么 |
|---|---|---|
| 1 | `omni.cpp:1553` `CV::imread` | 按 **BGR 3 通道 uint8** 读入。**只支持 jpg / jpeg / png / bmp** |
| 2 | `omni.cpp:459-460` | `mVisionWidth = mVisionHeight = image_size` → **无条件变成 560×560 正方形** |
| 3 | `omni.cpp:633-634` | 向上取整到 `image_size_unit = patch×merge = 28` 的倍数（560 已是倍数，恒等） |
| 4 | `omni.cpp:635-637` → `geometric.cpp:87-137` | **BGR→RGB** + 双线性缩放到 560×560 + `dest = normal × (src − mean)` 转 float —— 三步融合成一次 pass |
| 5 | `vision.py:2631` | 图内 `x = x * 2 − 1` |

第 2–4 步在 `Omni::paddleocrVLVisionProcess`（`omni.cpp:618`，由 `vision_type == "paddleocr_vl"`
在 `omni.cpp:1726` 派发过来）。

第 4 步的 mean/normal 来自 `llm_config.json`：

```json
"image_mean": [0.0, 0.0, 0.0],
"image_norm": [0.00392156862745098, ...]     // = 1/255
```

于是第 4 步输出 **`x/255 ∈ [0, 1]`**，第 5 步之后是 **`x/127.5 − 1 ∈ [−1, 1]`** ——
正是 SigLIP 的标准归一化 `(x/255 − 0.5) / 0.5`。

> `image_mean` 取全 0 是有意为之：这样 `(x − mean) × norm` 和 `x × norm − mean` 两种常见约定
> 结果重合，避免上下游约定不一致时静默算错。

### 4.3 必须知道的几个点

- ⚠️ **图片会被无条件拉伸成 560×560 正方形，长宽比丢失。**
  这会带来实质问题：**极端的宽高比会直接失败**，不是"精度略降"。
  实测一份 4243×380（≈11:1）的单行条带，拉伸后被横向压扁，输出是 `___` 或一长串 `0000…`
  乱码；而接近方形的图（1.4:1 左右）正常。
  **建议喂接近方形 / 页面形状的裁剪块，而不是把整页或很宽的整行直接丢进来。**
  上游用的是**动态分辨率**（`smart_resize` 保持比例、产出变分辨率的 patch 网格），
  静态方图做不到这一点，所以这是本导出版本的固有限制。
  变通办法：先自己按比例缩放并 padding 到 560×560（letterbox）再喂进来，
  实测能把上面那种条带从"完全读不出"救回到部分可读
  （但 90% 的视觉 token 会花在填充上，仍不理想）。
  这正是 PaddleOCR 3.x 流水线里前置版面检测（PP-DocLayout）存在的原因：**先切块，再逐块识别**。
- ⚠️ **只支持 `jpg` / `jpeg` / `png` / `bmp`。** webp / gif / tiff 会读失败
  （MNN CV 的 imgcodecs 只实现了这四种）。
- **灰度图 / 带 alpha 的图会被转成 3 通道 BGR**，alpha 通道丢弃，一般不影响识别。
- **按 8-bit 处理。** 16-bit PNG 会被当 uint8 读，数值可能不对。
- **插值是双线性 + half-pixel 对齐**（等价于 `align_corners=False`），
  与 `cv2.resize(..., interpolation=cv2.INTER_LINEAR)` 的默认行为一致。
- **视觉部分固定占 402 个 token**（1×`vision_start` + **400**×`image_pad` + 1×`vision_end`），
  与图片内容无关 —— 400 = (560/14)² / (2×2) = 1600/4。
  实测整段 prompt 为 413 token（含文本模板部分）。
- **每次会话一张图**，多图会受上一轮上下文影响（见 §7）。

### 4.4 想在 Python 侧复现同样的预处理

```python
import cv2, numpy as np

bgr = cv2.imread(path)                                            # BGR uint8
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)                        # 1) BGR -> RGB
img = cv2.resize(rgb, (560, 560), interpolation=cv2.INTER_LINEAR) # 2) 双线性到 560x560
x = img.astype(np.float32) / 255.0 * 2.0 - 1.0                    # 3) /255 再 x*2-1 -> [-1, 1]
x = x.transpose(2, 0, 1)[None]                                    # HWC -> CHW，即 visual.mnn 的输入
```

> 如果输入图**本来就是 560×560**，第 2 步是恒等变换、不引入插值误差。
> 本仓库验证时就是这么做的：先预缩放成 560×560，让 C++ 侧的 resize 退化成恒等，
> 从而保证 MNN 与 PyTorch 的像素**完全一致**（实测 maxdiff = 0），
> 这样比对结果才只反映模型本身的差异。

---

## 5. 怎么用

```bash
# 1) 写 prompt（<img> 路径 </img> + 任务提示词）
printf '<img>%s/table.png</img>Table Recognition:' "$PWD" > /tmp/prompt.txt

# 2) 推理（用贪心配置，结果可复现）
/path/to/MNN/build_omni/llm_demo /path/to/int8-int8/config_greedy.json /tmp/prompt.txt
```

输出：

```
<fcel>Product<fcel>Units<fcel>Revenue<nl><fcel>Sensor A1<fcel>12,400<fcel>$310,000<nl>
<fcel>Module B2<fcel>8,150<fcel>$652,000<nl><fcel>Kit C3<fcel>3,020<fcel>$151,000<nl>...
```

- **用 `config_greedy.json`**：`sampler_type=greedy`，结果可复现。
  llmexport 默认生成的 `config.json` 是 `sampler_type=mixed`（采样），**不可复现**，只适合交互式试用。
- 想调后端/线程数/精度：直接用 `config.json` 或 `config_greedy.json`，
  或在 C++ 里 `llm->set_config(...)`。图像后端在前端的 `mllm` 段里单独配。

### 目录里每个文件的作用

| 文件 | 必需 | 说明 |
|---|---|---|
| `config.json` | ✅ | 运行参数（后端、线程、采样器、文件名） |
| `llm_config.json` | ✅ | **模型架构**（`hidden_size` / `is_mrope` / `image_size` / `vision_start` / `chat_template`）。引擎会把它 merge 进配置，**缺了会报 `Unable to open llm_config file`** |
| `llm.mnn` + `llm.mnn.weight` | ✅ | 语言模型图 + 权重 |
| `visual.mnn` + `visual.mnn.weight` | ✅ | 视觉塔图 + 权重（文件名默认为 `visual.mnn`，`config.json` 里可用 `visual_model` 覆盖） |
| `embeddings_bf16.bin` | ✅ | 外置 token embedding（默认名，可用 `embedding_file` 覆盖） |
| `tokenizer.mtok` | ✅ | MNN 二进制 tokenizer |
| `config_greedy.json` | 建议 | 贪心版 `config.json`，对齐/回归测试用 |
| `llm.mnn.json` | 可选 | MNN 图的 JSON 文本形式，仅供查看/调试 |
| `export_args.json` | 可选 | 导出参数记录，用于复现 |

---

## 6. 上游 PR 状态：**尚未提交**

三个 commit **已经 rebase 到最新 `master`（`04ab6f2d`）**，但**还没有向上游 `alibaba/MNN` 提 PR**：

- 分支：`dr3334/MNN` → `feature/paddleocr-vl-support`
- 上游：`alibaba/MNN`
- 状态：**未开 PR**
- 规模：6 文件 / +231 / -1

```
[LLM:Doc]     Add PaddleOCR-VL-1.6 to the README news list
[LLM:Feature] Support PaddleOCR-VL export and runtime
[LLM:Bugfix]  Include added tokens when exporting tokenizer.mtok
```

因此：**官方 MNN 主线目前还不能直接导出/正确运行 PaddleOCR-VL**，
需要手动打上这三个 patch（或直接用这个分支）。

---

### 验证证据

**① 三任务与 PyTorch fp32 基线逐 token 一致**（rebased 后重新编译复测）：

| 任务 | prompt | token |
|---|---|---|
| 段落 OCR | `OCR:` | 147/147 ✓ |
| 表格 | `Table Recognition:` | 108/108 ✓ |
| 公式 | `Formula Recognition:` | 45/45 ✓ |

**② 运行时分支的反向对照**（证明位置修复是原因，不是巧合）：

| 模型 | `vision_type` | 结果 |
|---|---|---|
| 无 | 退回公共的 `defaultVisionProcess`（不调 `addPositionIds`） | **1/3** ❌ |
| 有 | `paddleocr_vl` → 走专门分支 | **3/3** ✅ |

**③ 重导可复现**：重跑导出，产物与验证用产物**逐字节一致**，只差 MNNConvert 随机生成的
模型 UUID（各 27 字节）和 `export_args.json` 里的 `dst_path`。

---

## 7. 已知限制

- **图像被无条件拉伸成 560×560 正方形**（长宽比丢失）。极端长宽比（很宽的整行）会被压扁，
  建议喂接近方形 / 页面形状的裁剪块。完整预处理链路与各格式支持见 **§4**。
- **视觉塔是静态 560×560**（40×40 patch → 400 token）。要支持其它分辨率需要重新导出
  （`PaddleOCRVLVision.image_size` 可配，需是 `patch_size * merge_size = 28` 的倍数）。
- **一次会话一张图**。多图会受上一轮上下文影响。
- 这是 PaddleOCR 3.x 流水线的**识别组件**，**不含前端的版面检测**（PP-DocLayout）。
  整个流水线是 `PP-DocLayout（版面分析）→ 按 block 裁剪 → PaddleOCR-VL 识别`。
  本模型的输入应当是**单个文本块/表格/公式区域**，而不是整页大图。
- 本仓库模型**不含** PP-DocLayoutV3；版面检测有独立的 MNN 版本
  （见 [PP-DocLayoutV3-mnn](https://www.modelscope.cn/models/dr3334/PP-DocLayoutV3-mnn)）。
- 速度数据是**纯 CPU / 4 线程**。GPU 后端（OpenCL / Metal / CUDA）未测试。

---

## 8. 验证方法

三个任务在固定图片上跑贪心解码，与 PyTorch fp32 基线逐字符比较：

| 任务 | 提示词 | 测试图 |
|---|---|---|
| 段落 OCR | `OCR:` | 英文财报段落（560×560） |
| 表格识别 | `Table Recognition:` | 5 行表格 |
| 公式识别 | `Formula Recognition:` | 含矩阵环境的多行公式 |

- 图片**预缩放到 560×560**，使 C++ 侧的 `CV::resize` 成为恒等变换，与 Python 侧像素完全一致（实测 maxdiff = 0）
- 基线用 `skip_special_tokens=False`，比较时统一去掉 `</s>`
- 文件：`out/omni_*.json`（每次运行的完整输出 + 速度）、`out/quant_matrix.json`（汇总）

---

## 9. 致谢

- 模型：[PaddlePaddle/PaddleOCR-VL-1.6](https://www.modelscope.cn/models/PaddlePaddle/PaddleOCR-VL-1.6)（Apache-2.0）
- 推理引擎：[alibaba/MNN](https://github.com/alibaba/MNN)
- 量化配置参考：[MNN/GLM-OCR-MNN](https://www.modelscope.cn/models/MNN/GLM-OCR-MNN)

## 引用

```bibtex
@misc{paddleocrvl16mnn,
  title  = {PaddleOCR-VL-1.6 MNN},
  author = {dr3334},
  year   = {2026},
  note   = {MNN conversion of PaddleOCR-VL-1.6}
}
```
