MinerU on 海光 Z100 DCU
从环境验证到生产级服务池的完整记录。包含 全部尝试路径——成功的和走死的、 每一条具体命令、每一个实测数字及其来源作业号,以及过程中被推翻的错误判断。
结论速览
| 项 | 结果 | 依据 |
|---|---|---|
| 服务池吞吐 | 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 / BF16 | 16.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 不需要它。
起点与问题定义
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) |
| torch | 2.7.1+das.opt1.dtk2604 |
| MinerU | 3.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 | 不跨节点共享 |
硬件能力探测
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 GEMM | 16.15 TFLOPS | 推理应走 FP16 |
| FP32 GEMM | 9.01 TFLOPS | |
| BF16 GEMM | 5.83 TFLOPS | 软件模拟,最慢 |
torch._int_mm | ✓ | INT8 可用 |
| SDPA | ⚠ 退化 | 回落 math 后端,无 flash / mem-efficient |
| 多卡 | ✓ 4 卡 | 每卡 16 GB(报告 17.2 GB) |
| hipBLASLt | ✕ 无 gfx906 kernel | 库中只有 gfx928,运行时刷 warning |
| FP8 / MFMA | ✕ | Vega20 架构限制 |
任何含一次性开销(kernel 自动调优、JIT、模型加载)的基准都必须预热,否则可能得出方向完全相反的结论。
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-python 的 ALL_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 连带不可用。
这是架构边界,配置改不了。
vLLM 移植可行性
5.1 关键区分
先厘清概念(这两个名字容易混):
- VLM = Vision-Language Model,一类模型(MinerU2.5-Pro-2605-1.2B 就是)
- vLLM = 一个推理引擎软件(PagedAttention)
MinerU 的选项分两层,差一个字母:
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 推理服务, 这条路值得继续。
打通解析链路:六个阻塞问题
每次报错都不同,说明在逐层前进。按实际遇到的顺序:
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/lib 进 ALL_LIBS |
No module named mineru.cli.api | 3.4.4 里模块叫 fast_api,修 mineru-api 包装器 |
解析质量验证
作业 <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」):
| 要素 | 结果 | 核对方式 |
|---|---|---|
| Markdown | 81,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 个区域。按内容实际形态选检验方法,不能用一种格式的缺席推断功能缺失。
性能测试
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 模型加载主导
最重要的一个数字:
模型加载占约 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 队列是生产可用路径,非降级方案。
但单文档对比会严重低估差距:
| 单文档 | 批量稳态 | |
|---|---|---|
| DCU | 146.8 s | 19.6 s |
| CPU | 315.4 s | 173.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 DCU | 233 docs/h | 735 docs/h | 19.6 s |
| 4 CPU worker | 63 docs/h | 83 docs/h | 173.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_DONE 并 RC=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 留给时延敏感的在线请求和公式密集语料。
服务池架构与实现
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 架构
设计依据(每条都来自实测,不是先验):
| 决策 | 依据 |
|---|---|
| 每卡一进程 | 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.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/h | 440 docs/h(+18%) |
| 负载分布 | 11 / 4 / 5 / 2 | 6 / 5 / 7 / 6 |
| p50 | 27.2 s | 38.0 s |
| p90 | 111.9 s | 121.7 s |
原始输出:data/loadtest.log、data/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}'"
| w0 | w1 | w2 | w3 | |
|---|---|---|---|---|
| 前 | 4839 | 4780 | 4754 | 4744 |
| 后 | 4837 | 4773 | 4762 | 4742 |
无增长。进程数稳定 4,无 spawn 子进程堆积。
10.5 汇总
走死的路径
记录失败路径与其代价,避免重复投入。
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 装 onnxruntime | ✕ | channels 访问超时 |
| Singularity 容器 | ✕ | Docker Hub 被墙 |
| Python 3.8 | ✕ | MinerU 需 ≥3.10 |
| Python 3.12 + onnx 1.17 强装 | ✕(当时) | 本次用重打标签手法解决了同类问题 |
被推翻的判断
工作过程中三次自我更正,都影响了结论方向。记录在此因为方法论比结论本身更有价值。
无预热测出 FP16 4.85 TFLOPS(比 FP32 的 8.91 还慢),会得出「gfx906 上 FP16 没意义」的结论。预热后 16.15 TFLOPS,是 FP32 的 1.8 倍。
任何含一次性开销的基准都必须预热。
最初说「vLLM 依赖 Triton,gfx906 上没戏」。这个推理是错的——没有分清依赖层次。
vLLM 的 PagedAttention 是手写 HIP kernel,不经 Triton。实测海光把
_C.abi3.so 编译进了 gfx906,paged_attention_v1 正常工作。
不要用一个组件的失败推断另一个组件,即使它们看起来相关。分层验证。
单文档 315.4 s vs 146.8 s = 2.1×,据此写了「CPU 队列只是稍慢」的建议。 批量稳态实测 173.8 s vs 19.6 s = 8.9×,差 4 倍。
原因:单文档两侧都被模型加载主导,而模型加载在 CPU/DCU 上耗时相近,稀释了真实差距。
含固定启动开销的对比必须用稳态数据。
初次只 grep $$ 显示块,得到「0 个公式」。实际这篇论文通篇用行内数学,
有 17 处 $...$,middle.json 里 38 个 inline_equation。
按内容实际形态选检验方法,不能用一种格式的缺席推断功能缺失。
裸金属部署
交付形态是裸金属,不含 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.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.log、data/soak.log |