技术档案 · 脱敏公开版

MinerU on 海光 Z100 DCU

从环境验证到生产级服务池的完整记录。包含 全部尝试路径——成功的和走死的、 每一条具体命令、每一个实测数字及其来源作业号,以及过程中被推翻的错误判断。

集群 测试集群(国产超算) 硬件 海光 Z100 DCU (gfx906) × 4 系统 CentOS 7.6 / glibc 2.17 软件栈 DTK 26.04 时间 2026-08-12 ~ 2026-08-13 交付形态 裸金属(systemd)
01

结论速览

结果依据
服务池吞吐955 docs/hour(4 卡,并发 12)soak.log
稳定性72 请求 0 失败 0 拒绝§10
故障自愈压测中 kill worker,自动重建,请求不丢§10.3
内存泄漏无(72 请求前后 RSS 零增长)§10.4
后端选择pipeline 3.9 s/页;VLM 14.93 s/页不建议§8.1
FP16 / FP32 / BF1616.15 / 9.01 / 5.83 TFLOPS§3
Triton / torch.compile✕ 架构边界 gfx906 不支持§4
vLLM HIP kernel✓ 可用 PagedAttention 实测可用§5
CPU 队列产出等价,稳态慢 8.9×§8.3
一句话

Z100 跑 MinerU 生产服务完全可行,走 pipeline 后端;Triton 生态用不了,但 MinerU 不需要它。

02

起点与问题定义

2.1 接手时的状态

前序工作已完成环境搭建并产出 V2 报告,声称「全部通过」。核查后发现一个关键缺口:

ssh -p <SSH_PORT> <CLUSTER_HOST> 'ls ~/.cache/modelscope/hub ~/models ~/output 2>/dev/null'
# 全部为空

MinerU 从未解析过任何文档。 V2 报告的「端到端测试」实际测的是 magika 文件类型检测——那是 MinerU 的第一步预处理,不是解析。模型一个都没下载。

2.2 已有资产(可用)

路径 / 版本
venv<USER_HOME>/mineru-venv-py310(Python 3.10.14)
torch2.7.1+das.opt1.dtk2604
MinerU3.4.4(editable,源码已改 magika 兼容)
glibc 2.28<USER_HOME>/glibc-2.28-rpm/(CentOS 8 RPM 提取)
包装器dcu-python / mineru / mineru-api

2.3 集群约束(profile 记录)

cat ~/<SKILL_CONFIG>
约束
内存上限cpus-per-task × 3569 MB
DCU 队列kshdnormal,QOS 强制 --gres=dcu:1
CPU 队列kshcnormal,若干节点,无 GRES 限制
计算节点外网❌ 无
/tmp不跨节点共享
03

硬件能力探测

3.1 第一次探针(有缺陷)

作业 <JOB_ID>

sbatch ~/probe_caps.slurm     # 见 cluster/probe_caps.py
torch 2.7.1 devices 4
arch: gfx906:sramecc-:xnack- mem GB: 17.2
[OK]   bf16 matmul: supported=True maxerr=0.250
[OK]   fp16 matmul: 4.85 TFLOPS          ← 可疑:比 FP32 还慢
[OK]   fp32 matmul: 8.91 TFLOPS
[FAIL] triton kernel: FileNotFoundError: '.../gcc-11.2.0-install/bin/gcc'
[OK]   SDPA: flash_avail=True mem_eff=True
[FAIL] torch.compile: FileNotFoundError: (同上)
[OK]   torch._int_mm: ok (512, 512)
[OK]   onnxruntime: 1.16.3 providers=['AzureExecutionProvider','CPUExecutionProvider']

FP16 比 FP32 慢是反常的——追查发现没做 warmup。

3.2 修正后的探针

作业 <JOB_ID>,加 5 次预热 + 30 次计时:

def bench(dt, n=2048, iters=30):
    a = torch.randn(n, n, device="cuda", dtype=dt)
    b = torch.randn(n, n, device="cuda", dtype=dt)
    for _ in range(5): c = a @ b          # ← 关键:预热
    torch.cuda.synchronize(); s = time.time()
    for _ in range(iters): c = a @ b
    torch.cuda.synchronize()
    return 2 * n**3 / ((time.time() - s) / iters) / 1e12
fp32: 9.01 TFLOPS
fp16: 16.15 TFLOPS      ← 是 FP32 的 1.8 倍
bf16: 5.73 TFLOPS
--- 强制 rocblas/hipblas 后端 ---
fp16: 16.12 TFLOPS      ← 无变化,说明本来就走 rocBLAS
bf16: 5.83 TFLOPS

3.3 最终能力表

能力结果说明
FP16 GEMM16.15 TFLOPS推理应走 FP16
FP32 GEMM9.01 TFLOPS
BF16 GEMM5.83 TFLOPS软件模拟,最慢
torch._int_mmINT8 可用
SDPA⚠ 退化回落 math 后端,无 flash / mem-efficient
多卡✓ 4 卡每卡 16 GB(报告 17.2 GB)
hipBLASLt✕ 无 gfx906 kernel库中只有 gfx928,运行时刷 warning
FP8 / MFMAVega20 架构限制
方法论教训

任何含一次性开销(kernel 自动调优、JIT、模型加载)的基准都必须预热,否则可能得出方向完全相反的结论。

04

Triton 三层错误链

这是本次最有价值的排查案例:真因藏在两层假错误之下

第 1 层:不存在的 gcc 路径

FileNotFoundError: '<USER_HOME>/software/gcc-11.2.0-install/bin/gcc'

用户 PATH 里有这个目录,但它不存在

echo $PATH | tr ':' '\n' | grep gcc
# <USER_HOME>/software/gcc-11.2.0-install/bin   ← 坏路径
ls <USER_HOME>/software/gcc-11.2.0-install/bin/gcc
# ls: cannot access ...: No such file or directory

看起来像「Triton 挂了」,其实只是路径坏。

第 2 层:系统 gcc 也是坏的

指向真实存在的 gcc-11.2.0 后:

cc1: error while loading shared libraries: libisl.so.15: cannot open shared object file

逐个实测集群上的编译器:

echo "int main(){return 0;}" > /tmp/t.c
for cc in <SOFTWARE_ROOT>/compiler/gcc-11.2.0/bin/gcc \
          <SOFTWARE_ROOT>/compiler/gcc-12.2.0/bin/gcc \
          <SOFTWARE_ROOT>/compiler/gcc-13.3.0/bin/gcc \
          /opt/rh/devtoolset-7/root/usr/bin/gcc \
          <SOFTWARE_ROOT>/compiler/rocm/dtk-26.04/llvm/bin/clang; do
  $cc /tmp/t.c -o /tmp/t.out 2>/dev/null && echo "OK   $cc" || echo "FAIL $cc"
done
编译器结果
gcc-11.2.0✕ 缺 libisl.so.15
gcc-12.2.0
gcc-13.3.0✕ 缺 libisl.so.15
devtoolset-7
DTK clang

第 3 层:真因

用 gcc-12.2.0 后(作业 <JOB_ID>):

loc("probe_triton.py":4:0): error: unsupported target: 'gfx906'
RuntimeError: PassManager::run failed

海光官方 Triton 也一样

海光发布了 DCU 专版 Triton,值得单独验证:

curl -sL "https://download.sourcefind.cn:65024/directlink/4/triton/DAS1.8/"
# triton-3.1.0+das.opt1.dtk2604.torch271-cp310-cp310-manylinux_2_28_x86_64.whl

装到独立目录避免污染工作 venv:

cp triton-3.1.0+...manylinux_2_28_x86_64.whl triton-3.1.0+...manylinux2014_x86_64.whl
pip install --no-deps --target=<USER_HOME>/dcu-triton-test 

首次运行缺 libgcvm.so.17git,定位到 dtk-26.04/dcc/gcvm/lib(不在包装器库路径里), 补进 dcu-pythonALL_LIBS 后(作业 <JOB_ID>):

triton: 3.1.0 from <USER_HOME>/dcu-triton-test/triton/__init__.py
arch: gfx906:sramecc-:xnack-
[FAIL] RuntimeError: PassManager::run failed
error: unsupported target: 'gfx906'      ← 与 PyPI 版完全相同

读源码确认架构白名单:

grep -ohE "gfx9[0-9]{2}[a-z]*" triton/backends/amd/compiler.py | sort -u
# gfx928 gfx936 gfx940 gfx941 gfx942
grep -ohE "gfx9[0-9]{2}[a-z]*" triton/backends/hcu/compiler.py | sort -u
# gfx928 gfx936 gfx938 gfx940 gfx941 gfx942

gfx906 不在任何列表中。

⚠ 注意一个误导性证据

strings libtriton.so | grep gfx906 能搜到 gfx906。那是 LLVM 上游的 AMDGPU target 列表, LLVM 认识 ≠ Triton 支持。必须实测,不能靠 strings 推断。

结论

Triton 3.x(PyPI 版与海光 DCU 版)均不支持 gfx906,torch.compile 连带不可用。 这是架构边界,配置改不了。

05

vLLM 移植可行性

5.1 关键区分

先厘清概念(这两个名字容易混):

  • VLM = Vision-Language Model,一类模型(MinerU2.5-Pro-2605-1.2B 就是)
  • vLLM = 一个推理引擎软件(PagedAttention)

MinerU 的选项分两层,差一个字母:

后端(用什么方法解析) ├── pipeline 专用小模型:版面 + OCR + 公式 + 表格 ├── vlm-engine 用 VLM 大模型端到端读整页 └── hybrid-engine └─ vlm-engine 内部再选引擎 ├── transformers 纯 PyTorch,最慢 ├── vllm-engine ← vLLM 在这里 └── lmdeploy

5.2 关键发现:kernel 编译进了 gfx906

海光也发布了 DCU 版 vLLM:

curl -sL "https://download.sourcefind.cn:65024/directlink/4/vllm/DAS1.8/"
# vllm-0.11.0+das.opt1.dtk2604.torch271-cp310-cp310-manylinux_2_28_x86_64.whl

解包检查编译目标:

strings vllm/_C.abi3.so     | grep -oE "gfx[0-9]{3,4}" | sort -u
# gfx906 gfx926 gfx928 gfx936 gfx938
strings vllm/_moe_C.abi3.so | grep -oE "gfx[0-9]{3,4}" | sort -u
# gfx906 gfx926 gfx928 gfx936 gfx938

海光在 Triton 里排除了 gfx906,却在 vLLM 的 HIP kernel 里保留了它。

5.3 实测(作业 <JOB_ID>

from vllm import _custom_ops as ops
# PagedAttention —— vLLM 的核心
ks = torch.tensor(1.0, device="cuda"); vs = torch.tensor(1.0, device="cuda")
ops.paged_attention_v1(out, q, kc, vc, nkvh, 1.0/(hd**0.5),
                       btab, slen, bs, bs*2, None, "auto", ks, vs, 0,0,0,64,0)
arch: gfx906:sramecc-:xnack-
[OK] import vllm 0.11.0
[OK] vllm._C loaded
[OK] rms_norm (HIP): maxerr=0.0010
[OK] rotary_embedding (HIP): ran
[OK] paged_attention_v1 (HIP) ★: out(4, 8, 64) finite=True

5.4 结论与未竟事项

Z100 上 vLLM 有真实通路——核心 HIP kernel 全部可用,与 Triton 的结论方向相反。

未解决:平台检测失败(is_rocm: False)。vLLM 靠 import amdsmi 判断 ROCm:

# vllm/platforms/__init__.py:109
import amdsmi
amdsmi.amdsmi_init()

而 DTK 26.04 只有旧的 rocm_smi.hyhal/rocm_smi/bin/rsmiBindings.py)。 需要 amdsmi shim 或 patch rocm_platform_plugin() 才能走通完整推理。

对 MinerU 无收益:vLLM 的价值在 PagedAttention + 连续批处理,针对高并发长文本 生成;MinerU 的 VLM 负载是单页图 + 短输出,用不上。若要在 Z100 上做 LLM 推理服务, 这条路值得继续。

06

打通解析链路:六个阻塞问题

每次报错都不同,说明在逐层前进。按实际遇到的顺序:

6.1 模型缺失

modelscope 会 import torch,在登录节点必炸(无 DCU 驱动)。改用 huggingface_hub (纯 Python)+ hf-mirror:

# 探测可达性
curl -s -o /dev/null -w "%{http_code}" https://hf-mirror.com/     # 200
curl -s -o /dev/null -w "%{http_code}" https://huggingface.co/    # 被墙
# cluster/dl_models.py
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HF_HOME"] = "<USER_HOME>/mineru-models/hf"
from huggingface_hub import snapshot_download
for repo in ["opendatalab/MinerU2.5-Pro-2605-1.2B",
             "opendatalab/PDF-Extract-Kit-1.0"]:
    snapshot_download(repo, max_workers=8)

下载 20 GB。配置本地路径:

{
  "models-dir": {
    "pipeline": "<USER_HOME>/mineru-models/models/OpenDataLab--PDF-Extract-Kit-1.0/snapshots/master",
    "vlm":      "<USER_HOME>/mineru-models/models/OpenDataLab--MinerU2.5-Pro-2605-1.2B/snapshots/master"
  },
  "model-source": "local",
  "device-mode": "cuda"
}

6.2 libssl.so.3 not found

File "mineru/cli/fast_api.py", line 19, in 
    import uvicorn
ImportError: libssl.so.3: cannot open shared object file
Error: Local mineru-api exited before becoming healthy.

根因:MinerU CLI 会 fork 一个 mineru-api 子进程,用裸 sys.executable 启动, 绕过 dcu-python 包装器

当时的绕法:改走 do_parse() Python API,不经 CLI。 后来的根治(服务化必须):把 /usr/local/lib64 加进包装器 ALL_LIBS

6.3 operator torchvision::nms does not exist

torchvision 0.21.0 是 PyPI 通用版,与 DCU torch 2.7.1 ABI 不匹配。

DCU 版 torchvision 在 sourcefind 的 vision/ 目录(不是 torchvision/):

curl -sL "https://download.sourcefind.cn:65024/directlink/4/vision/DAS1.8/"
# torchvision-0.22.0+das.opt1.dtk2604.torch271-cp310-...whl   ← 与 torch271 配套

pip 拒绝 manylinux_2_28(系统 glibc 2.17)。重打标签强装——运行时本来就在 glibc 2.28 下,pip 的平台检查是多余的:

W=torchvision-0.22.0+das.opt1.dtk2604.torch271-cp310-cp310-manylinux_2_28_x86_64.whl
cp $W ${W/manylinux_2_28/manylinux2014}
pip install --no-deps --force-reinstall ${W/manylinux_2_28/manylinux2014}

6.4 缺 OCR / 版面依赖

pip install shapely pyclipper omegaconf einops ftfy

6.5 BrokenProcessPool

concurrent.futures.process.BrokenProcessPool:
  A process in the process pool was terminated abruptly

根因:MinerU 渲染 PDF 用 spawn 进程池:

# mineru/utils/pdf_image_tools.py:160
if start_method != "spawn":
    return ProcessPoolExecutor(max_workers=max_workers,
                               mp_context=multiprocessing.get_context("spawn"))

spawn 重新 exec sys.executable,丢掉全部库路径。

解法——sitecustomize.py,Python 启动时自动 import,全局生效:

# /lib/python3.10/site-packages/sitecustomize.py
import os
_w = "<USER_HOME>/mineru-venv-py310/bin/dcu-python"
if os.path.exists(_w):
    import multiprocessing
    multiprocessing.set_executable(_w)
    try:
        multiprocessing.get_context("spawn").set_executable(_w)
    except Exception:
        pass

验证:

dcu-python -c "
import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor
def f(x): return x*x
if __name__=='__main__':
    with ProcessPoolExecutor(2, mp_context=mp.get_context('spawn')) as ex:
        print(list(ex.map(f,[1,2,3])))"
# [1, 4, 9]
⚠ 调用方注意

调用 MinerU 的脚本必须if __name__ == "__main__": 保护。 spawn 子进程会 import 主模块;没有保护则整个脚本被重复执行(日志里 [info] 打印两次),随后 BrokenProcessPool。MinerU 文档未说明这点。

6.6 Unsupported model IR version: 10

onnxruntime.capi.onnxruntime_pybind11_state.Fail:
  Load model from .../PP-LCNet_x1_0_table_cls.onnx failed:
  Unsupported model IR version: 10, max supported IR version: 9

onnxruntime 1.16.3 上限 IR v9,MinerU 的表格分类模型是 IR v10。

V2 报告判定「onnxruntime ≥1.17 仅发布 manylinux_2_28,不兼容」——但用 6.3 的 重打标签手法可解,而且这不是可选优化,是必需项

curl -sL "https://pypi.tuna.tsinghua.edu.cn/simple/onnxruntime/" \
  | grep -oE 'href="[^"]*onnxruntime-1\.20\.1-cp310-cp310-manylinux_2_27_x86_64[^"]*"'
curl -sL -o ort.whl "https://pypi.tuna.tsinghua.edu.cn/packages/63/47/.../onnxruntime-1.20.1-...whl"
cp ort.whl onnxruntime-1.20.1-cp310-cp310-manylinux2014_x86_64.whl
pip install --no-deps --force-reinstall onnxruntime-1.20.1-cp310-cp310-manylinux2014_x86_64.whl

6.7 附带修复

问题修法
onnxruntime 刷 pthread_setaffinity 报错OMP/ORT/OPENBLAS/MKL_NUM_THREADS=4
libgcvm.so.17git 缺失dtk-26.04/dcc/gcvm/libALL_LIBS
No module named mineru.cli.api3.4.4 里模块叫 fast_api,修 mineru-api 包装器
07

解析质量验证

作业 <JOB_ID>,Mooncake 论文 23 页 0.58 MB。

dcu-python parse_api.py ~/sourcecode/Mooncake/Mooncake-v3.pdf ~/parse-out/api-pipeline pipeline
[RESULT] backend=pipeline elapsed=146.8s
[OUT] .../Mooncake-v3.md chars=81072
processing-window multi-file infer finished, cost: 90.52, speed: 0.254 page/s

产出文件:

Mooncake-v3.md                  81 KB    Markdown 正文
Mooncake-v3_content_list.json  121 KB    结构化内容
Mooncake-v3_middle.json        2.1 MB    中间表示
Mooncake-v3_layout.pdf         835 KB    版面可视化
Mooncake-v3_span.pdf           832 KB    span 可视化
images/                        26 文件

逐项核对(不满足于「作业返回 0」):

要素结果核对方式
Markdown81,072 字符标题层级、上标署名 ♠♡、摘要结构正确
表格3 个 HTML抽样核对 LRUCache/LFUCache 数值行完整
图像23 引用 / 26 文件images/ 实际落盘
标题31 个层级与原文一致
行内公式17 处 LaTeX$T_{queue}$$\mathrm{MLP}$$T_{prefill}$
显示公式0 个原文确实用行内数学,非漏检

首页输出片段:

# Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving

Ruoyu Qin♠♡1 Zheming Li♠1 ... ♠Moonshot AI ♡Tsinghua University

## Abstract

Mooncake is the serving platform for Kimi, a leading LLM service provided by Moonshot AI...
核查教训

初次只 grep $$ 显示块得到「0 个公式」,据此差点判定公式识别失效。 实际这篇系统论文通篇用行内数学,middle.json 有 38 个 inline_equation, MFR 识别 19 个区域。按内容实际形态选检验方法,不能用一种格式的缺席推断功能缺失。

08

性能测试

8.1 三种后端

后端作业速度结论
pipeline (DCU)<JOB_ID>3.9 s/页(90.5 s / 23 页)生产选用
pipeline (CPU)<JOB_ID>10.1 s/页(231.3 s)产出等价,见 §8.3
VLM (transformers)<JOB_ID>14.93 s/页50 分钟超时未完成

VLM 慢 3.8 倍且 gfx906 上无加速手段(Triton 不可用 ⇒ vllm-engine 路径走不通), 生产不建议启用

验证 VLM 确实在算而非挂死:

ssh <COMPUTE_NODE> "cat /proc/<PID>/status | grep -E '^State|^Threads'; ls /proc/<PID>/fd | grep -c kfd"
# State: R (running)   Threads: 44   kfd handles: 1

8.2 模型加载主导

最重要的一个数字

146.8s
单文档(冷启动)
22.5s
批量中的同一文档
~85%
耗时占比:模型加载

模型加载占约 85%。 这直接否定「一文档一作业」的架构——85% 机时花在反复加载 同一批模型上。必须常驻进程批量喂。

8.3 CPU 队列等价性

产出逐字节比对:

diff ~/parse-out/cpu/Mooncake-v3/auto/Mooncake-v3.md \
     ~/parse-out/api-pipeline/Mooncake-v3/auto/Mooncake-v3.md
# 5 行差异:
# 98,99c98
# < ## Listing 1: Request samples.
# ---
# > Listing 1: Request samples.

81,074 vs 81,072 字符,差异仅一处标题层级。 CPU 队列是生产可用路径,非降级方案。

但单文档对比会严重低估差距

单文档批量稳态
DCU146.8 s19.6 s
CPU315.4 s173.8 s
比值2.1×8.9×

单文档两侧都被模型加载主导(CPU/DCU 上耗时相近),稀释真实差距 4 倍

CPU 路径代价集中在公式识别:MFR 在 DCU 上 5.86 it/s,CPU 上 3.85 s/it(约 22×)。

8.4 批量吞吐(无 HTTP)

作业 <JOB_ID>(DCU)、<JOB_ID>(CPU):

dcu-python throughput.py ~/pdf-corpus ~/parse-out/tput pipeline 4
配置冷启动计入稳态稳态单文档
4 DCU233 docs/h735 docs/h19.6 s
4 CPU worker63 docs/h83 docs/h173.8 s

4 卡负载均衡:各 worker 总耗时 176.4 / 177.4 / 185.3 / 185.7 s,偏差 < 6%

发现的 bug:批量跑完 worker 不退出,4 进程各占 3.8 GB RSS 挂死。 MinerU 的 _get_pdf_render_executor() 创建常驻 ProcessPoolExecutor, 非 daemon 子进程阻止父进程退出。修法:

from mineru.utils.pdf_image_tools import shutdown_pdf_render_executor
shutdown_pdf_render_executor()   # worker 收尾时调用

修复后 CPU 批量作业正常打印 BENCH_DONERC=0 退出。

8.5 队列选型

实测时的集群负载:

sinfo -p kshdnormal -o "%t %D" -h | sort    # alloc 735 / idle 2  (满载)
sinfo -p kshcnormal -o "%t %D" -h | sort    # idle 1106           (空闲)

总吞吐 = 单节点速度 × 能拿到的节点数

  • 少量空闲 DCU 节点 × 735 = 约 1,470 docs/h
  • 若干空闲 CPU 节点 × 83 = 约 91,663 docs/h

量级差距来自可用性,不是算力。大批量离线解析应铺 CPU 队列; DCU 留给时延敏感的在线请求和公式密集语料。

09

服务池架构与实现

9.1 网络可达性(决定架构)

作业 <JOB_ID>。先验证计算节点能否对外提供服务:

# 计算节点上起服务,写出 endpoint
IP=$(hostname -I | awk '{print $1}'); PORT=$((30000 + RANDOM % 5000))
dcu-python svc_hello.py $PORT &
echo "$IP:$PORT" > ~/svc_endpoint.txt

# 登录节点访问
curl --max-time 8 "http://$(cat ~/svc_endpoint.txt)/"
# MINERU_SVC_ALIVE on <COMPUTE_NODE>      ← 通
测试结果
计算节点绑 0.0.0.0 自测
登录节点 → 计算节点端口
计算节点 → 外网✕(预期)

集群内网互通,与「计算节点无外网」是两件事。服务池因此能真正对外服务。

9.2 架构

┌───────────────────────────────┐ 客户端 ──HTTP──►│ 网关 :38080 │ │ 健康探测 · 最小负载调度 │ │ 每 worker 限流 · 失败重试 │ └───────┬───────────────────────┘ │ 按空闲槽位分发 ┌───────────┬───────┴───────┬───────────┐ ▼ ▼ ▼ ▼ worker :38100 :38101 :38102 :38103 DCU 0 DCU 1 DCU 2 DCU 3 │ │ │ │ └───────────┴───────┬───────┴───────────┘ ▼ 注册表(共享目录,文件 mtime = 心跳)

设计依据(每条都来自实测,不是先验):

决策依据
每卡一进程MinerU 模型非线程安全,单进程内并发会串行化
HIP_VISIBLE_DEVICES 钉卡显存隔离,一个 worker OOM 不影响其他
每 worker 并发上限 = 1压测得出,见 §10.1
文件 mtime 做心跳worker 自注册自摘除,网关只扫目录,无需额外服务发现
失败重试到别的 worker解析是幂等的,可安全重试

9.3 MinerU API 端点

grep -n "@app\.\(get\|post\)" -A3 mineru/cli/fast_api.py
路由用途
POST /file_parse同步解析
POST /tasks异步提交
GET /tasks/{id} /tasks/{id}/result异步查询
GET /health健康检查(网关用)

9.4 关键实现片段

网关调度scripts/mineru_gateway.py):

def _acquire(exclude=()):
    """Reserve a slot on the least-loaded healthy worker, waiting if all busy."""
    deadline = time.time() + SLOT_WAIT_TIMEOUT
    with _cv:
        while True:
            cands = [(v["inflight"], v["served"], u)
                     for u, v in _workers.items()
                     if v["healthy"] and u not in exclude
                     and v["inflight"] < MAX_INFLIGHT]      # ← 限流
            if cands:
                cands.sort()
                url = cands[0][2]
                _workers[url]["inflight"] += 1
                return url
            if time.time() >= deadline: return None
            _cv.wait(2.0)                                    # ← 排队而非失败

失败重试

for attempt in range(MAX_ATTEMPTS):
    url = _acquire(exclude=tuple(tried))     # 排除已失败的 worker
    ...
    except urllib.error.HTTPError as e:
        if e.code < 500:                     # 4xx 是客户端问题,不重试
            return 
        _release(url, False)
    tried.append(url)

worker 监督cluster/mineru_worker.sh):

while true; do
  mineru-api --host 0.0.0.0 --port $PORT &
  APID=$!
  # 等健康后再注册,避免流量打到还在加载模型的 worker
  for i in $(seq 1 90); do
    curl -sf --max-time 3 "http://127.0.0.1:$PORT/health" >/dev/null && READY=1 && break
    sleep 2
  done
  [ "$READY" = 1 ] && echo "{\"url\":\"$URL\",...}" > "$REGFILE"
  # 心跳:健康就 touch,网关靠 mtime 判活
  while kill -0 $APID <PID>>/dev/null; do
    curl -sf --max-time 5 ".../health" >/dev/null && touch "$REGFILE"
    sleep 10
  done
  rm -f "$REGFILE"; sleep 5      # 崩了就重启
done

9.5 启动

sbatch ~/mineru_pool.slurm        # cluster/mineru_pool.slurm
[t+95s] healthy workers: 4
=== POOL READY ===
{"pool_size": 4, "healthy": 4, "max_inflight_per_worker": 1, ...}

从登录节点验证:

curl -s "http://<SERVICE_ENDPOINT>/pool/status" | python3 -m json.tool
10

服务池压测

10.1 网关 v1 vs v2

同样 24 请求 / 并发 8:

python3 loadtest.py <SERVICE_ENDPOINT> ~/pdf-corpus 8 2
指标v1(无限流无重试)v2(生产版)
错误率8.3%(2 × 502)0.0%
吞吐374 docs/h440 docs/h(+18%)
负载分布11 / 4 / 5 / 26 / 5 / 7 / 6
p5027.2 s38.0 s
p90111.9 s121.7 s

原始输出:data/loadtest.logdata/loadtest2.log

限流反而提升吞吐——MinerU 模型非线程安全、请求串行处理,往忙碌 worker 堆请求 只会内部排队。偏斜从 5.5:1 收敛到 1.4:1。

p50 上升是预期且正确的:v1 的低 p50 是少数请求抢到空闲 worker 的假象, 代价是 p90 拖长和 8.3% 失败。v2 让所有请求公平排队。

10.2 超订压测(3× worker 数)

48 请求 / 并发 12:

python3 loadtest.py <SERVICE_ENDPOINT> ~/pdf-corpus 12 4
total=48  ok=48  failed=0  error_rate=0.0%
wall=180.9s  throughput=955 docs/hour  concurrency=12
latency  p50=32.4s  p90=70.4s  p99=131.1s  min=11.9s  max=131.1s
per-worker distribution:  11 / 11 / 14 / 12

压测中途的池状态:

"in_flight": 4  "waiting_for_slot": 8  "queue_peak": 8  "retries": 0  "rejected": 0

背压正确:在途硬顶 4,超出的 8 个排队而非失败。过载降速,不丢请求。

并发 8→12 吞吐 440→955 docs/h,说明并发 8 时管道未打满。 955 已超过纯批处理的 735——批处理按卡静态分片有空转,服务池按空闲槽位动态分发。

10.3 故障自愈(压测进行中执行)

PID=$(python3 -c "import json;print(json.load(open('.../<COMPUTE_NODE>-dcu3.json'))['pid'])")
ssh <COMPUTE_NODE> "kill -9 $PID"        # PID <PID>

50 秒后:

before: "healthy": 3  "total_served": 5   "total_failed": 0
after:  "healthy": 4  "total_served": 16  "total_failed": 0

验证确实重建(PID 变化 + 日志):

cat ~/mineru-registry/<COMPUTE_NODE>-dcu3.json
# {"url":"http://<SERVICE_ENDPOINT>",...,"pid":<PID>}     ← 原 <PID>

grep -a dcu3 pool-*.err | grep -E "restart|READY|exited"
# [worker dcu3] exited, restarting in 5s (total restarts: 1)
# [worker dcu3] starting on http://<SERVICE_ENDPOINT> (restart #1)
# [worker dcu3] READY -> registered http://<SERVICE_ENDPOINT>

在真实负载下验证,total_failed 全程 0,请求未丢失。

10.4 泄漏检查(72 请求前后)

ssh <COMPUTE_NODE> "ps -u <USER> -o rss,cmd --sort=-rss | grep -a fast_api | awk '{print \$1/1024}'"
w0w1w2w3
4839478047544744
4837477347624742

无增长。进程数稳定 4,无 spawn 子进程堆积。

10.5 汇总

72
累计请求(24+48)
0
失败
0
拒绝
955/h
峰值吞吐
11

走死的路径

记录失败路径与其代价,避免重复投入。

11.1 patchelf 烧路径进 ELF ✕ 失败

动机ld-linux 包装器只覆盖顶层进程,想用 patchelf 把库路径写进 Python 二进制,让所有子进程自动继承。理论上比包装器干净。

patchelf --set-interpreter $GLIBC_DIR/ld-linux-x86-64.so.2 python3.10
Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37:
  _dl_call_libc_early_init: Assertion `sym != NULL' failed!

加 rpath 后仍 segfault,连 ldd 都崩:

patchelf --set-interpreter ... --force-rpath --set-rpath "$G:/usr/local/lib64:/usr/lib64" python3.10
./python3.10 -c "print(1)"        # Segmentation fault
ldd python3.10                    # Segmentation fault

分项定位:

测试结果
只改 rpath,保留系统 interpreter✓ 正常
只换 interpreter✕ 断言失败
两者都改✕ segfault
结论

glibc 2.28 的 ld.so 与系统 libc 2.17 混用不可行。已从备份回滚。 改 ELF 这条路在 glibc 大版本跨越时走不通,只能用包装器 + sitecustomize.py

11.2 VLM 后端 ⚠ 能跑但不实用

14.93 s/页,50 分钟超时未完成 23 页。gfx906 上无加速手段: Triton 不可用 ⇒ vllm-engine / lmdeploy 引擎路径都走不通,只能 transformers 单序列推理。pipeline 后端已能出高质量结果且快 3.8 倍。

11.3 vLLM 完整推理 ⏸ 未完成,非失败

核心 HIP kernel 实测可用(§5),但平台检测卡在 import amdsmi。 DTK 26.04 只有旧的 rocm_smi。需要写 shim 或 patch。这条路是通的,只是没走完。

11.4 V2 报告中已记录的失败路径

路径结果原因
Conda 装 onnxruntimechannels 访问超时
Singularity 容器Docker Hub 被墙
Python 3.8MinerU 需 ≥3.10
Python 3.12 + onnx 1.17 强装✕(当时)本次用重打标签手法解决了同类问题
12

被推翻的判断

工作过程中三次自我更正,都影响了结论方向。记录在此因为方法论比结论本身更有价值。

12.1「FP16 无收益」← 未预热

无预热测出 FP16 4.85 TFLOPS(比 FP32 的 8.91 还慢),会得出「gfx906 上 FP16 没意义」的结论。预热后 16.15 TFLOPS,是 FP32 的 1.8 倍

教训

任何含一次性开销的基准都必须预热。

12.2「vLLM 在 gfx906 上没戏」← 用 Triton 的失败推断 vLLM

最初说「vLLM 依赖 Triton,gfx906 上没戏」。这个推理是错的——没有分清依赖层次。 vLLM 的 PagedAttention 是手写 HIP kernel,不经 Triton。实测海光把 _C.abi3.so 编译进了 gfx906,paged_attention_v1 正常工作。

教训

不要用一个组件的失败推断另一个组件,即使它们看起来相关。分层验证。

12.3「CPU 只慢 2.1 倍」← 用单文档数据外推批量

单文档 315.4 s vs 146.8 s = 2.1×,据此写了「CPU 队列只是稍慢」的建议。 批量稳态实测 173.8 s vs 19.6 s = 8.9×,差 4 倍。

原因:单文档两侧都被模型加载主导,而模型加载在 CPU/DCU 上耗时相近,稀释了真实差距。

教训

含固定启动开销的对比必须用稳态数据。

12.4「公式识别失效」← 只查了一种格式

初次只 grep $$ 显示块,得到「0 个公式」。实际这篇论文通篇用行内数学, 有 17 处 $...$,middle.json 里 38 个 inline_equation

教训

按内容实际形态选检验方法,不能用一种格式的缺席推断功能缺失。

13

裸金属部署

交付形态是裸金属,不含 Slurm。

13.1 关键建议:换 glibc ≥ 2.28 的发行版

集群上那套 ld-linux 包装器是为绕开 CentOS 7 的 glibc 2.17 而生的, 代价是子进程盲区(§6.2、§6.5)。裸金属你控制 OS,这个坑本可以不踩。

DCU torch 实际要求 GLIBC_2.25

objdump -T torch.libs/libevent_core*.so* | grep -oE "GLIBC_[0-9.]+" | sort -u -V | tail -3
# GLIBC_2.14
# GLIBC_2.17
# GLIBC_2.25      ← 真实要求

sort -V,不能用 sort -u——后者按字典序会把 2.9 排在 2.25 之后, 得出「只需 GLIBC_2.9」的假结论。

另外:两个 torch wheel(manylinux_2_17 / _2_28)md5 完全相同7b098c2f9c036fcdb9ae244228bac822),只是标签不同,别指望换标签那个能省事。

选 Rocky 8+ / Ubuntu 20.04+ / CentOS 8+,sitecustomize.py、包装器、 重打标签这些手法全部不需要。

13.2 部署

./mineru-baremetal-deploy.sh preflight    # 检查 glibc / DTK / venv / 模型
sudo ./mineru-baremetal-deploy.sh install # 生成 systemd unit
sudo ./mineru-baremetal-deploy.sh start
./mineru-baremetal-deploy.sh status

systemd 负责开机自启与崩溃重启(Restart=always)。 TimeoutStartSec=300 给模型加载留时间;注册脚本等健康检查通过后才注册, 避免流量打到还在加载模型的 worker。

13.3 调用

curl -X POST http://:8000/file_parse \
     -F "files=@paper.pdf" -F "backend=pipeline"

curl http://:8000/pool/status

响应头 X-MinerU-Worker 标明处理的 worker,X-MinerU-Attempt 标明第几次尝试。

13.4 监控接入

字段告警建议
healthy< 期望值 报警
waiting_for_slot持续 > 2× worker 数 → 需扩容
total_failed增速异常报警
retries突增说明有 worker 不稳定
rejected> 0 立即排查

13.5 扩容

横向线性扩展,不需改网关:新机器部署 worker(不启网关), 注册表目录指向共享存储(NFS/GPFS)上同一路径,网关自动发现。

单机上限由卡数决定。若 CPU 成瓶颈(版面分析、OCR 前处理是 CPU 密集的), 降低 *_NUM_THREADS 或减少 worker 数。

14

附录:命令速查

14.1 环境

# 连接
ssh -p <SSH_PORT> <CLUSTER_HOST>

# 作业里必须先
module load compiler/dtk/26.04
source <USER_HOME>/mineru-venv-py310/bin/activate

# 离线声明(计算节点无外网)
export HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1
export MINERU_MODEL_SOURCE=local
export MINERU_TOOLS_CONFIG_JSON=<USER_HOME>/mineru.json

# 唯一可用的 gcc(gcc-11.2.0 / 13.3.0 都缺 libisl.so.15)
export CC=<SOFTWARE_ROOT>/compiler/gcc-12.2.0/bin/gcc

# 多 worker 共享机器时限制线程
export OMP_NUM_THREADS=4 ORT_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4

14.2 作业模板

#!/bin/bash
#SBATCH -p kshdnormal
#SBATCH --gres=dcu:4              # QOS 强制至少 1
#SBATCH --cpus-per-task=32
#SBATCH --mem=111gb               # 上限 = cpus × 3569 MB
#SBATCH --time=02:00:00
#SBATCH -o job-%j.out
#SBATCH -e job-%j.err
module load compiler/dtk/26.04
source <USER_HOME>/mineru-venv-py310/bin/activate
...
rc=$?; echo "EXIT_RC=$rc"; exit $rc     # ★ 必须,否则 sacct 状态会骗人

先验证配额不真排队:

sbatch --test-only job.slurm
# "Job N to start at ..."  → 配额合法
# "Requested node configuration is not available" → 无空闲节点(配额没问题)
# "too much memory" → 超配额

14.3 服务池

~/mineru_pool_ctl.sh status       # 作业 + 存活 worker + 网关健康
~/mineru_pool_ctl.sh scale 2      # 保持 2 个池作业(每个 4 卡)
~/mineru_pool_ctl.sh stop         # 停止并清注册表
~/mineru_pool_ctl.sh endpoints    # 打印网关地址

14.4 压测

python3 loadtest.py    
python3 loadtest.py <SERVICE_ENDPOINT> ~/pdf-corpus 12 4

14.5 排查

# ★ 不要只看 sacct 的 State,必须读日志
sacct -j <ID> -X -o State,Elapsed,ExitCode -P -n

# onnxruntime 会刷屏,过滤掉
grep -av "pthread_setaffinity" job-<ID>.err | tail -30

# 确认进程在算还是挂死
ssh  "cat /proc//status | grep -E '^State|^Threads'"
ssh  "ls /proc//fd | grep -c kfd"     # >0 说明持有 DCU 句柄

# 查 wheel 的真实 glibc 要求(必须 sort -V)
objdump -T .so | grep -oE "GLIBC_[0-9.]+" | sort -u -V | tail -3

14.6 manylinux 重打标签强装

运行时已在 glibc 2.28 下时,pip 的平台检查是多余的:

cp pkg-1.0-cp310-cp310-manylinux_2_28_x86_64.whl \
   pkg-1.0-cp310-cp310-manylinux2014_x86_64.whl
pip install --no-deps --force-reinstall pkg-1.0-cp310-cp310-manylinux2014_x86_64.whl

14.7 海光 DCU 软件下载

# 目录结构(注意 torchvision 在 vision/ 下)
curl -sL "https://download.sourcefind.cn:65024/directlink/4/"
curl -sL "https://download.sourcefind.cn:65024/directlink/4/vision/DAS1.8/"
curl -sL "https://download.sourcefind.cn:65024/directlink/4/vllm/DAS1.8/"
curl -sL "https://download.sourcefind.cn:65024/directlink/4/triton/DAS1.8/"

版本必须与 torch 严格配套,如 ...das.opt1.dtk2604.torch271

·

数据来源索引

数据作业号原始文件
硬件能力(初测)<JOB_ID>data/job-logs.txt
硬件能力(含预热)<JOB_ID>同上
首次成功解析<JOB_ID>data/sample-parse-output.md
CPU 单文档<JOB_ID>data/job-logs.txt
VLM 后端<JOB_ID>同上
DCU 批量吞吐<JOB_ID>同上
CPU 批量吞吐<JOB_ID>同上
海光 Triton<JOB_ID>同上
vLLM kernel<JOB_ID>同上
服务池 v1<JOB_ID>data/loadtest.log
服务池 v2<JOB_ID>data/loadtest2.logdata/soak.log
文档生成:2026-08-13 · 数据来自脱敏后的实测记录 · 公开报告版本