跳转到内容

三引擎后端的源码实现与调度

三引擎后端的源码实现与调度

学习目标

读完本章后,你将能够:

  • 理解 MinerU 三引擎后端的源码组织结构
  • 掌握引擎调度的核心入口与路由逻辑
  • 分析 ModelSingleton 的缓存复用机制
  • 阅读和理解各引擎的 analyze 入口函数

前置知识


1. 源码目录结构

MinerU 的解析后端代码组织如下:

mineru/
├── backend/
│ ├── pipeline/ # Pipeline 引擎
│ │ ├── pipeline_analyze.py # 入口函数
│ │ ├── model_init.py # 模型初始化
│ │ ├── model_json_to_middle_json.py # 结果转换
│ │ ├── pipeline_middle_json_mkcontent.py # Markdown 生成
│ │ └── batch_analyze.py # 批量推理
│ ├── vlm/ # VLM 引擎
│ │ ├── vlm_analyze.py # 入口函数
│ │ ├── model_output_to_middle_json.py # 结果转换
│ │ └── vlm_middle_json_mkcontent.py # Markdown 生成
│ ├── hybrid/ # Hybrid 引擎
│ │ ├── hybrid_analyze.py # 入口函数
│ │ ├── hybrid_model_output_to_middle_json.py
│ │ └── hybrid_magic_model.py
│ └── office/ # Office 原生解析
│ ├── docx_analyze.py
│ ├── pptx_analyze.py
│ └── xlsx_analyze.py
└── cli/
└── common.py # 引擎调度入口

2. 引擎调度核心:do_parse

引擎调度的核心逻辑在 mineru/cli/common.pydo_parse 函数中。伪代码:

def do_parse(
file_path: Path,
pdf_bytes: bytes,
parse_method: str, # "auto", "ocr", "txt"
backend: str, # "pipeline", "vlm-auto-engine", "hybrid-auto-engine" 等
# ... 其他参数
):
# 1. 根据文件后缀选择解析路径
suffix = guess_suffix_by_path(file_path)
if suffix in office_suffixes:
# Office 文档 → 原生解析
if suffix == "docx":
middle_json = office_docx_analyze(pdf_bytes, ...)
markdown = office_union_make(middle_json, ...)
elif suffix == "pptx":
middle_json = office_pptx_analyze(pdf_bytes, ...)
elif suffix == "xlsx":
middle_json = office_xlsx_analyze(pdf_bytes, ...)
elif suffix in pdf_suffixes + image_suffixes:
# PDF/图片 → 根据后端选择解析引擎
if backend.startswith("hybrid"):
# Hybrid 引擎
doc_analyze_fn = _load_hybrid_analyze_entrypoint(...)
middle_json = doc_analyze_fn(pdf_bytes, ...)
markdown = vlm_union_make(middle_json, ...)
elif backend.startswith("vlm"):
# VLM 引擎
middle_json = vlm_doc_analyze(pdf_bytes, ...)
markdown = vlm_union_make(middle_json, ...)
else:
# Pipeline 引擎
middle_json = pipeline_doc_analyze(pdf_bytes, ...)
markdown = pipeline_union_make(middle_json, ...)
# 2. 处理输出(写文件、可视化等)
_process_output(middle_json=middle_json, markdown=markdown, ...)

关键设计点

  • 后缀检测自动路由到正确的解析器
  • 每个引擎输出统一的 middle_json 格式
  • 不同引擎复用对应的 union_make 函数生成 Markdown

3. ModelSingleton:模型缓存复用

三个引擎都使用单例模式缓存模型,避免重复加载:

class ModelSingleton:
_instance = None
_models = {}
_lock = threading.RLock()
def __new__(cls, *args, **kwargs):
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def get_model(self, lang=None, formula_enable=None, table_enable=None):
key = (lang, formula_enable, table_enable)
with self._lock:
if key not in self._models:
self._models[key] = custom_model_init(
lang=lang,
formula_enable=formula_enable,
table_enable=table_enable,
)
return self._models[key]

设计优势

  • 线程安全:使用 threading.RLock 保护模型初始化
  • 配置隔离:不同配置(语言、公式、表格开关)缓存不同实例
  • 懒加载:首次调用时才加载模型,不浪费启动时间

4. Pipeline 引擎实现要点

mineru/backend/pipeline/pipeline_analyze.py
def pipeline_doc_analyze(pdf_bytes, ...):
# 1. PDF 分页
pdf_doc = pdfium.PdfDocument(pdf_bytes)
page_images = load_images_from_pdf_doc(pdf_doc)
# 2. 逐页处理
for page_img in page_images:
# 布局检测
layout_result = layout_model.predict(page_img)
# 区域分类 → 路由到不同子模型
for block in layout_result.blocks:
if block.type == "text":
ocr_result = ocr_model.predict(block.image)
elif block.type == "equation":
mfr_result = mfr_model.predict(block.image)
elif block.type == "table":
tsr_result = tsr_model.predict(block.image)
# 3. 转换为中间表示
middle_json = model_json_to_middle_json(...)
return middle_json

5. VLM 引擎实现要点

mineru/backend/vlm/vlm_analyze.py
def vlm_doc_analyze(pdf_bytes, ...):
# 1. 获取 VLM 模型(支持多种后端)
model = ModelSingleton().get_model(
backend=backend, # "transformers", "vllm", "lmdeploy", "mlx", "http-client"
model_path=model_path,
server_url=server_url,
)
# 2. 分页推理
for page_img in page_images:
# VLM 端到端推理
result = model.predict(page_img)
page_blocks.append(result)
# 3. 转换为中间表示
middle_json = model_output_to_middle_json(page_blocks)
return middle_json

VLM 后端适配

  • transformers:原生 PyTorch 推理
  • vllm:vLLM 框架推理(Linux)
  • lmdeploy:LMDeploy 框架推理(Windows)
  • mlx:Apple MLX 框架推理(macOS)
  • http-client:HTTP 调用 OpenAI 兼容 API

6. Hybrid 引擎实现要点

mineru/backend/hybrid/hybrid_analyze.py
def hybrid_doc_analyze(pdf_bytes, ...):
# 1. VLM 布局检测
vlm_layout = vlm_model.detect_layout(page_img)
# 2. 区域分类
for block in vlm_layout.blocks:
if block.type in TEXT_TYPES:
# 文本区域 → Pipeline OCR
text_result = pipeline_ocr(block.image)
else:
# 复杂区域 → VLM 理解
vlm_result = vlm_model.understand(block.image)
# 3. 融合输出
middle_json = merge_results(text_result, vlm_result)
return middle_json

7. 陷阱与对策

陷阱对策
Hybrid 引擎未安装 torch检查依赖,安装 mineru[pipeline]
ModelSingleton 缓存导致配置混用不同配置使用不同的 key
引擎入口函数签名不一致通过 do_parse 统一封装参数传递
模型加载失败无明确报错查看 loguru 日志输出

8. 设计权衡

单例 vs 每次新建

方案优势劣势
单例缓存模型只加载一次,节省时间首次加载慢,内存占用固定
每次新建配置灵活,用完即释放每次加载模型耗时

取舍:MinerU 选择单例缓存,因为模型加载耗时远大于推理耗时,且服务端场景下模型常驻内存是合理的。


参考来源