Skip to main content

MinerU vlm-engine 后端代码走读

基于 MinerU 源码,逐层追踪 vlm-engine 后端的执行流程。


与 pipeline 的本质区别​

维度pipelinevlm-engine
核心思路多个小模型串行,各司其职一个大 VLM 端到端理解
步骤布局 → 公式 → OCR → 表格(7 步)两步:页面理解 → 后处理
模型7 个小模型(~500M)1 个 VLM(1.2B)
输入逐页渲染的 PIL Image同
输出每页的 layout_res → 逐步填充每页的 page_blocks(JSON)直接出来
PDF 分类需要(txt/ocr)不需要 — VLM 自己判断
公式单独模型VLM 直接输出 LaTeX
表格4 个步骤(方向→分类→结构→OCR)VLM 直接输出 HTML
硬件CPU 可跑,GPU ≥4GBGPU ≥8GB(vLLM/LMDeploy)
精度~85%~95%

核心简化:VLM 把 pipeline 的 7 个步骤压缩为了 1 次推理。


调用链总览​

aio_do_parse(backend="vlm-transformers")
│
└── _async_process_vlm() # cli/common.py:426
│
├── prepare_env() # 创建输出目录
│
├── aio_vlm_doc_analyze() # vlm_analyze.py
│ │
│ ├── ModelSingleton.get_model() # 获取/初始化 MinerUClient
│ │ └── 根据 backend 创建不同引擎
│ │ ├─ transformers → Qwen2VL 本地加载
│ │ ├─ vllm-engine → vLLM LLM 引擎
│ │ ├─ vllm-async-engine → vLLM AsyncLLM
│ │ ├─ lmdeploy-engine → LMDeploy VLAsyncEngine
│ │ └─ http-client → 远程 API 调用
│ │
│ ├── [每批最多 64 页,滑动窗口]
│ │ ├── pypdfium2 渲染 → PIL Image[]
│ │ └── predictor.batch_two_step_extract() → page_blocks
│ │
│ └── finalize_middle_json()
│ ├── build_para_blocks_from_preproc() # preproc_blocks → para_blocks
│ ├── merge_para_text_blocks() # 段落合并
│ ├── cross_page_table_merge() # 跨页表格合并
│ ├── apply_title_leveling() # 标题层级
│ └── cleanup_internal_metadata() # 清理内部字段
│
└── _process_output(mode="vlm") # 输出文件
├── union_make(MM_MD) → .md
├── union_make(CONTENT_LIST) → _content_list.json
└── union_make(CONTENT_LIST_V2) → _content_list_v2.json

第一层:_async_process_vlm()​

位置:mineru/cli/common.py:426

async def _async_process_vlm(output_dir, pdf_file_names, pdf_bytes_list, backend, ...):
parse_method = "vlm" # 固定,不像 pipeline 有 auto/txt/ocr 之分
f_draw_span_bbox = False # VLM 没有 span 级别的检测

关键差异:

  • 不需要 PDF 分类 — 不调 _get_ocr_enable(),VLM 自己能理解图片
  • 不需要 f_draw_span_bbox — pipeline 有文字行级别的 span,VLM 没有
  • process_mode="vlm" — 输出时选择 vlm_union_make() 而非 pipeline_union_make()

逐文件处理(和 pipeline 的多文件批处理不同)​

for idx, pdf_bytes in enumerate(pdf_bytes_list):
local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name, "vlm")
image_writer, md_writer = FileBasedDataWriter(...), FileBasedDataWriter(...)

middle_json, infer_result = await aio_vlm_doc_analyze(
pdf_bytes, image_writer=image_writer, backend=backend, server_url=server_url, **kwargs,
)
_process_output(..., middle_json, infer_result, process_mode="vlm")

pipeline 那边是多文档混合批处理(把多个文档的页面打包到同一个 batch),VLM 这里是逐文档处理,每个文档独立调一次 aio_vlm_doc_analyze。


第二层:ModelSingleton.get_model() — 引擎初始化​

位置:mineru/backend/vlm/vlm_analyze.py(ModelSingleton 类)

这是 VLM 后端的核心——根据 backend 参数初始化不同的推理引擎。

支持的后端​

backend 参数引擎说明
transformersHuggingFace Transformers本地加载,适合开发调试
mlx-engineApple MLXmacOS Apple Silicon 专用
vllm-enginevLLM (sync)高性能推理,生产环境首选
vllm-async-enginevLLM (async)异步版本,支持并发
lmdeploy-engineLMDeploy国产 GPU 友好(昇腾/寒武纪等)
http-client远程 HTTP连接已有的 mineru-api 实例

初始化示例:vllm-engine​

if backend == "vllm-engine":
kwargs["gpu_memory_utilization"] = set_default_gpu_memory_utilization()
kwargs["model"] = model_path
if enable_custom_logits_processors():
kwargs["logits_processors"] = [MinerULogitsProcessor] # 自定义解码约束
vllm_llm = vllm.LLM(**kwargs)

统一客户端:MinerUClient​

所有后端最终都封装为 MinerUClient(来自 mineru_vl_utils 包):

predictor = MinerUClient(
backend=backend,
model=model, # transformers 用
processor=processor, # transformers 用
vllm_llm=vllm_llm, # vllm-engine 用
vllm_async_llm=..., # vllm-async-engine 用
lmdeploy_engine=..., # lmdeploy-engine 用
server_url=..., # http-client 用
enable_table_formula_eq_wrap=True,
image_analysis=True,
enable_cross_page_table_merge=True,
)

模型下载​

模型路径:HuggingFace opendatalab/MinerU2.5-Pro-2605-1.2B 或 ModelScope OpenDataLab/MinerU2.5-Pro-2605-1.2B

if backend not in ["http-client"] and not model_path:
model_path = auto_download_and_get_model_root_path("/", "vlm")

第三层:aio_doc_analyze() — 核心推理​

位置:mineru/backend/vlm/vlm_analyze.py

3.1 流程概览​

async def aio_doc_analyze(pdf_bytes, image_writer, predictor, backend, ...):
pdf_doc = open_pdfium_document(pdfium.PdfDocument, pdf_bytes)
middle_json = init_middle_json()
results = []

# 滑动窗口:每批最多 64 页
for window_start in range(0, page_count, window_size):
window_end = min(page_count - 1, window_start + window_size - 1)

# Step 1: pypdfium2 渲染页面 → PIL Image[]
images_list = await aio_load_images_from_pdf_bytes_range(
pdf_bytes,
start_page_id=window_start,
end_page_id=window_end,
)
images_pil_list = [item["img_pil"] for item in images_list]

# Step 2: VLM 推理 —— 关键!
window_results = await predictor.aio_batch_two_step_extract(
images=images_pil_list,
image_analysis=True,
)

# Step 3: 结果转换为 middle_json
append_page_blocks_to_middle_json(
middle_json, window_results, images_list,
pdf_doc, image_writer, page_start_index=window_start,
)

# Step 4: 后处理
finalize_middle_json(middle_json["pdf_info"])

和 pipeline 的最大差异在这里:pipeline 的一个 batch 走了 7 步模型调用,VLM 只走 batch_two_step_extract() 一步。

3.2 batch_two_step_extract() — "两步提取"​

这是 MinerUClient(来自 mineru_vl_utils 包)的方法。顾名思义分两阶段:

  1. Step 1 — 页面结构提取:VLM 分析整页图片,识别所有区域(文本、标题、表格、公式、图片、页眉页脚等),输出 JSON 块列表
  2. Step 2 — 内容精炼:对每个块的内容做二次处理——表格 HTML 格式化、公式 LaTeX 规范化、图片裁剪等

输出结构(每页):

[
{
"type": "title", # 块类型
"bbox": [0.1, 0.05, 0.9, 0.08], # 相对坐标 (0~1)
"content": "第一章 概述",
"sub_type": None,
},
{
"type": "text",
"bbox": [0.1, 0.1, 0.9, 0.3],
"content": "正文内容...",
},
{
"type": "table",
"bbox": [0.1, 0.35, 0.9, 0.6],
"content": "<table><tr><td>...</td></tr></table>",
},
{
"type": "equation",
"bbox": [0.1, 0.65, 0.9, 0.7],
"content": "E = mc^2",
},
...
]

关键:bbox 是相对坐标(0~1 比例),后面在 MagicModel 中会乘以页面实际宽高转换为绝对像素坐标。


第四层:blocks_to_page_info() → MagicModel​

位置:mineru/backend/vlm/model_output_to_middle_json.py

4.1 转换入口​

def blocks_to_page_info(page_blocks, image_dict, page, image_writer, page_index):
scale = image_dict["scale"]

# 获取页面实际尺寸
width, height = map(int, page.get_size())

# MagicModel 完成类型映射 + 结构重组
magic_model = MagicModel(page_blocks, width, height)

# 提取各类块
image_blocks = magic_model.get_image_blocks()
table_blocks = magic_model.get_table_blocks()
...

4.2 MagicModel.__init__() — VLM 输出的结构化​

位置:mineru/backend/vlm/vlm_magic_model.py

这是 VLM 和 pipeline 的关键桥梁。MagicModel 将 VLM 输出的非结构化 JSON 块转换为 pipeline 兼容的内部格式。

类型映射​

VLM 输出的 type 字段 → 内部 BlockType / ContentType:

VLM type内部 BlockTypeContentType
text, ref_text, header, footer, page_number, aside_text, page_footnote保持原样CONTENT_TYPE.TEXT
titleBlockType.TITLETEXT
image_caption, table_captionBlockType.CAPTIONTEXT
image_footnote, table_footnoteBlockType.FOOTNOTETEXT
imageBlockType.IMAGE_BODYIMAGE
tableBlockType.TABLE_BODYTABLE
chartBlockType.CHART_BODYCHART
equationBlockType.INTERLINE_EQUATIONINTERLINE_EQUATION
code, algorithmBlockType.CODE_BODYTEXT
listBlockType.LISTTEXT

坐标转换​

# VLM 输出相对坐标 (0~1),转换为绝对像素坐标
x1, y1, x2, y2 = block_bbox
x_1, y_1, x_2, y_2 = (
int(x1 * width), int(y1 * height),
int(x2 * width), int(y2 * height),
)

公式处理​

如果文本块内含 \(...\) 标记(VLM 输出的行内公式),MagicModel 会自动拆分为多个 span:

if block_content.count("\\(") == block_content.count("\\)"):
# 将 "文本 \(E=mc^2\) 文本" 拆为:
# [{type: TEXT, content: "文本 "}, {type: INLINE_EQUATION, content: "E=mc^2"}, ...]
for match in re.finditer(r"\\\((.+?)\\\)", block_content):
...

图片/表格/公式截图​

for span in all_spans:
if span["type"] in [IMAGE, TABLE, CHART, INTERLINE_EQUATION]:
span = cut_image_and_table(span, page_pil_img, ..., scale=scale)

和 pipeline 一样,这些区域从整页图中裁剪出来保存为独立图片。

列表块合并​

fix_list_blocks(list_blocks, text_blocks, ref_text_blocks)

如果文本块 80% 以上被 list 块覆盖,则合并入 list 块。


第五层:finalize_middle_json() — 后处理​

位置:mineru/backend/vlm/model_output_to_middle_json.py:113

def finalize_middle_json(pdf_info_list):
build_para_blocks_from_preproc(pdf_info_list) # preproc_blocks → para_blocks
merge_para_text_blocks(pdf_info_list) # 段落合并
if table_enable:
cross_page_table_merge(pdf_info_list) # 跨页表格合并
apply_title_leveling_to_pdf_info(pdf_info_list) # 标题层级后处理
cleanup_internal_para_block_metadata(pdf_info_list) # 清理内部字段

和 pipeline 相比:

  • 不需要 para_split(段落拆分)—— VLM 已经在输出时做了
  • 需要 cross_page_table_merge——因为跨页表格两次推理是独立的,需要后合并
  • 需要 apply_title_leveling——VLM 可能给出不连续的标题层级

第六层:_process_output(mode="vlm")​

位置:mineru/cli/common.py:259

if process_mode == "vlm":
make_func = vlm_union_make # ← 和 pipeline 不同
elif process_mode == "pipeline":
make_func = pipeline_union_make

vlm_union_make() 在 mineru/backend/vlm/vlm_middle_json_mkcontent.py,逻辑和 pipeline 版本类似但有些差异——比如 VLM 输出的表格 HTML 中公式用 <eq>...</eq> 标记需要替换为 LaTeX 定界符:

def _replace_eq_tags_in_table_html(html):
return re.sub(
r'<eq>(.*?)</eq>',
lambda m: f" {inline_left_delimiter}{unescape(m.group(1))}{inline_right_delimiter} ",
html,
)

完整时序​

aio_do_parse(backend="vlm-transformers")
│
└── _async_process_vlm()
│
├── prepare_env() → output/doc/vlm/
│
└── aio_vlm_doc_analyze()
│
├── ModelSingleton.get_model("transformers")
│ └── Qwen2VLForConditionalGeneration.from_pretrained() # 加载 1.2B 模型
│
├── [window 1: pages 1-64]
│ ├── aio_load_images_from_pdf_bytes_range()
│ │ └── pypdfium2 渲染 → PIL Image[64]
│ │
│ └── predictor.aio_batch_two_step_extract(images)
│ Step 1: VLM 推理 → 每页的 JSON 块列表
│ Step 2: 内容精炼(表格 HTML 格式化、公式规范化)
│ → page_blocks[64]
│
├── append_page_blocks_to_middle_json()
│ └── MagicModel(page_blocks, width, height)
│ ├── 类型映射 (VLM type → BlockType)
│ ├── 坐标转换 (相对→绝对)
│ ├── 行内公式拆分
│ ├── 图片/表格截图裁剪
│ └── 列表块合并
│
├── [window 2: pages 65-100] ...
│
└── finalize_middle_json()
├── build_para_blocks_from_preproc()
├── merge_para_text_blocks()
├── cross_page_table_merge()
├── apply_title_leveling()
└── cleanup_internal_metadata()
│
└── _process_output(mode="vlm")
├── draw_layout_bbox() → _layout.pdf
├── vlm_union_make(MD) → .md
├── vlm_union_make(CONTENT_LIST) → _content_list.json
└── vlm_union_make(CONTENT_LIST_V2) → _content_list_v2.json

与 pipeline 的关键代码对比​

入口路由​

# cli/common.py:724
if backend == "pipeline":
_process_pipeline(...) # 多文档混合批处理,双线程流水线
else:
if backend.startswith("vlm-"):
_async_process_vlm(...) # 逐文档处理

推理核心​

pipelinevlm-engine
函数batch_image_analyze()predictor.batch_two_step_extract()
输入PIL Image + ocr_enable + langPIL Image
输出images_layout_res (含 text/table/... 逐字段填充)page_blocks (结构化 JSON)
步骤布局→公式→分类→表格方向→表格类型→表格OCR→表格结构→正文OCR1 步

后处理​

pipelinevlm-engine
para_split 段落拆分 ✅para_split 段落拆分 ❌(VLM 已做)
cross_page_table_merge ❌(单独 table_continuation.py)cross_page_table_merge ✅
apply_title_leveling ❌apply_title_leveling ✅

关键文件索引​

文件职责
cli/common.py_async_process_vlm/_process_vlm:VLM 后端入口
backend/vlm/vlm_analyze.py核心:ModelSingleton(引擎管理)、aio_doc_analyze/doc_analyze(推理循环)
backend/vlm/vlm_magic_model.pyVLM 输出 → 内部格式转换(MagicModel)
backend/vlm/model_output_to_middle_json.pyblocks_to_page_info、finalize_middle_json
backend/vlm/vlm_middle_json_mkcontent.pyunion_make:中间 JSON → .md / content_list.json
backend/vlm/utils.py辅助:logits processors、默认参数
model/vlm/vllm_server.pyvLLM 服务端启动脚本
model/vlm/lmdeploy_server.pyLMDeploy 服务端启动脚本
utils/enum_class.pyModelPath.vlm_root_hf/modelscope