mirror of
https://github.com/opendatalab/MinerU.git
synced 2026-10-25 09:35:17 +08:00
feat(onnx): automatically select CPU or DirectML devices
Add auto/cpu/gpu configuration, Windows adapter discovery, isolated rec calibration, atomic preference persistence, and vendor session presets. Route layout, rec, and full MFR batches through DirectML with CPU fallback. Use mutually exclusive Windows x64 DirectML dependencies and guard ORT/Magika distribution identity. Include calibration fixtures, three-machine results, the 0.8 threshold with ten UHD 770 repeats, and installation documentation. Validation: 762 MinerU tests and 71 DocVortex tests passed; dependency checks and Windows Python 3.10-3.14 resolution passed. The 19-page OCR result matches the CPU baseline. DocVortex PyPI acceptance remains pending a compatible release; pinned source acceptance passed.
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
{
|
||||
"date": "2026-10-18",
|
||||
"scope": "Windows x64 installation acceptance using pinned DocVortex source; cross-platform resolution and wheel checks only",
|
||||
"docvortex": {
|
||||
"version": "0.5.16",
|
||||
"repository": "https://github.com/myhloli/DocVortex",
|
||||
"commit": "545570c4bffa2a668b7db03b5d3117e7a13faf8c",
|
||||
"native_extension": false,
|
||||
"source_dirty_before_build": false
|
||||
},
|
||||
"packages": {
|
||||
"mineru": "4.0.12",
|
||||
"docvortex": "0.5.16",
|
||||
"mineru-magika": "1.0.3",
|
||||
"mineru-vl-utils": "2.0.7",
|
||||
"onnxruntime-directml": "1.24.4"
|
||||
},
|
||||
"python": "3.14.7 (tags/v3.14.7:823f032, Aug 5 2026, 10:51:32) [MSC v.1944 64 bit (AMD64)]",
|
||||
"owners": {
|
||||
"magika": [
|
||||
"mineru-magika"
|
||||
],
|
||||
"onnxruntime": [
|
||||
"onnxruntime-directml"
|
||||
]
|
||||
},
|
||||
"ort": {
|
||||
"version": "1.24.4",
|
||||
"path": "D:\\projects\\20260914magicpdf\\Magic-PDF\\.venv\\Lib\\site-packages\\onnxruntime\\__init__.py",
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"wheel_sha256": {
|
||||
"docvortex-0.5.16-py3-none-any.whl": "94cde0320fac4be4af474cac78adc33bb57affc2d9d2aa56a02ba263169b785b",
|
||||
"mineru-4.0.12-py3-none-any.whl": "199ac6229cf443672dc16f49d6e377cbdd599d4702cb25274529090ea5f38eb0",
|
||||
"mineru_magika-1.0.3-py3-none-any.whl": "1b67e05769f48c28d4d5dda0869ece4f4acce769ec7ab6ed169ed34ee5b58343",
|
||||
"mineru_vl_utils-2.0.7-py3-none-any.whl": "3bf184c359d69e78d0509f58324c58ad323037cf97bd4086cacb9caf53db0db7"
|
||||
},
|
||||
"pip_check": {
|
||||
"success": true,
|
||||
"checked_packages": 159,
|
||||
"installation_resolved_packages": 121
|
||||
},
|
||||
"tests": {
|
||||
"mineru": 762,
|
||||
"docvortex": 71,
|
||||
"warnings": "Two third-party deprecations in MinerU suite; no failures"
|
||||
},
|
||||
"rec": {
|
||||
"threshold": 0.8,
|
||||
"new_processes": 3,
|
||||
"validation_passed": true,
|
||||
"runs": [
|
||||
{
|
||||
"cpu_elapsed": 0.259271000046283,
|
||||
"gpu_elapsed": 0.14191849995404482,
|
||||
"ratio": 0.5473751400222572,
|
||||
"selected": "gpu",
|
||||
"outputs_match": true,
|
||||
"ort_version": "1.24.4",
|
||||
"ort_path": "D:\\projects\\20260914magicpdf\\Magic-PDF\\.venv\\Lib\\site-packages\\onnxruntime\\__init__.py",
|
||||
"gpu_providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cpu_elapsed": 0.26189600001089275,
|
||||
"gpu_elapsed": 0.13717320002615452,
|
||||
"ratio": 0.5237697407384962,
|
||||
"selected": "gpu",
|
||||
"outputs_match": true,
|
||||
"ort_version": "1.24.4",
|
||||
"ort_path": "D:\\projects\\20260914magicpdf\\Magic-PDF\\.venv\\Lib\\site-packages\\onnxruntime\\__init__.py",
|
||||
"gpu_providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cpu_elapsed": 0.262668399955146,
|
||||
"gpu_elapsed": 0.1418222999200225,
|
||||
"ratio": 0.53992905101733,
|
||||
"selected": "gpu",
|
||||
"outputs_match": true,
|
||||
"ort_version": "1.24.4",
|
||||
"ort_path": "D:\\projects\\20260914magicpdf\\Magic-PDF\\.venv\\Lib\\site-packages\\onnxruntime\\__init__.py",
|
||||
"gpu_providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"native": {
|
||||
"html_type": "html",
|
||||
"html_pages": 1,
|
||||
"html_text_preserved": true,
|
||||
"pdf_pages": 1,
|
||||
"pdf_markdown_characters": 71977,
|
||||
"html_execution": {
|
||||
"tier": "flash",
|
||||
"parse_mode": "txt"
|
||||
},
|
||||
"pdf_execution": {
|
||||
"tier": "flash",
|
||||
"parse_mode": "txt"
|
||||
}
|
||||
},
|
||||
"document": {
|
||||
"pages": 19,
|
||||
"wall_s": 145.46046650002245,
|
||||
"comparison": {
|
||||
"pages_equal": true,
|
||||
"markdown_text_equal": true,
|
||||
"markdown_bytes_equal_with_matching_serialization": true,
|
||||
"baseline_newline": "CRLF",
|
||||
"initial_newline": "LF",
|
||||
"baseline_sha256": "6fa409c5986b5207a4cadff8d106a063c977180758babe615fc5ce58ebd86b49",
|
||||
"current_lf_sha256": "7e541eba607dc12b2d2a6aa48ed5041be926db9f66d855f45e32c494b6fb6418",
|
||||
"current_crlf_sha256": "6fa409c5986b5207a4cadff8d106a063c977180758babe615fc5ce58ebd86b49",
|
||||
"note": "Initial helper used write_bytes while the baseline helper used Windows write_text; only newline serialization differed. The comparison preserves the original LF output and writes a separate CRLF artifact."
|
||||
},
|
||||
"sessions": [
|
||||
{
|
||||
"key": "('onnx', 'cpu', ('ocr', 0.5, 'ch', 1.5, True), OnnxSelection(device='gpu', adapter=GpuAdapter(index=0, luid='00000000:0000f3be', name='Intel(R) Arc(TM) 130T GPU (16GB)', vendor_id=32902, device_id=32081, uma=None, preference=0), reason='configured_gpu'))",
|
||||
"name": "text_recognizer",
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "('onnx', 'cpu', ('ocr', 0.5, 'ch', 1.5, True), OnnxSelection(device='gpu', adapter=GpuAdapter(index=0, luid='00000000:0000f3be', name='Intel(R) Arc(TM) 130T GPU (16GB)', vendor_id=32902, device_id=32081, uma=None, preference=0), reason='configured_gpu'))",
|
||||
"name": "text_detector",
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "('onnx', 'cpu', ('layout', 'cpu'), OnnxSelection(device='gpu', adapter=GpuAdapter(index=0, luid='00000000:0000f3be', name='Intel(R) Arc(TM) 130T GPU (16GB)', vendor_id=32902, device_id=32081, uma=None, preference=0), reason='configured_gpu'))",
|
||||
"name": "session",
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "('onnx', 'cpu', ('ocr', 0.5, 'ch', 1.6, False), OnnxSelection(device='gpu', adapter=GpuAdapter(index=0, luid='00000000:0000f3be', name='Intel(R) Arc(TM) 130T GPU (16GB)', vendor_id=32902, device_id=32081, uma=None, preference=0), reason='configured_gpu'))",
|
||||
"name": "text_recognizer",
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "('onnx', 'cpu', ('ocr', 0.5, 'ch', 1.6, False), OnnxSelection(device='gpu', adapter=GpuAdapter(index=0, luid='00000000:0000f3be', name='Intel(R) Arc(TM) 130T GPU (16GB)', vendor_id=32902, device_id=32081, uma=None, preference=0), reason='configured_gpu'))",
|
||||
"name": "text_detector",
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "('onnx', 'cpu', ('mfr', 'cpu'), OnnxSelection(device='gpu', adapter=GpuAdapter(index=0, luid='00000000:0000f3be', name='Intel(R) Arc(TM) 130T GPU (16GB)', vendor_id=32902, device_id=32081, uma=None, preference=0), reason='configured_gpu'))",
|
||||
"name": "session",
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "('onnx', 'cpu', 'table_cls', OnnxSelection(device='gpu', adapter=GpuAdapter(index=0, luid='00000000:0000f3be', name='Intel(R) Arc(TM) 130T GPU (16GB)', vendor_id=32902, device_id=32081, uma=None, preference=0), reason='configured_gpu'))",
|
||||
"name": "sess",
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"platform_matrix": [
|
||||
{
|
||||
"platform": "linux",
|
||||
"python": "3.10",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.23.2"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"python": "3.11",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"python": "3.12",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"python": "3.13",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"python": "3.14",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos-intel",
|
||||
"python": "3.10",
|
||||
"extra": "base",
|
||||
"success": false,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.23.2"
|
||||
],
|
||||
"errors": [
|
||||
"cryptography==50.0.2: no wheel for macos-intel Python 3.10"
|
||||
],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos-intel",
|
||||
"python": "3.11",
|
||||
"extra": "base",
|
||||
"success": false,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.23.2"
|
||||
],
|
||||
"errors": [
|
||||
"cryptography==50.0.2: no wheel for macos-intel Python 3.11"
|
||||
],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos-intel",
|
||||
"python": "3.12",
|
||||
"extra": "base",
|
||||
"success": false,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.23.2"
|
||||
],
|
||||
"errors": [
|
||||
"cryptography==50.0.2: no wheel for macos-intel Python 3.12"
|
||||
],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos-intel",
|
||||
"python": "3.13",
|
||||
"extra": "base",
|
||||
"success": false,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.23.2"
|
||||
],
|
||||
"errors": [
|
||||
"cryptography==50.0.2: no wheel for macos-intel Python 3.13"
|
||||
],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos-intel",
|
||||
"python": "3.14",
|
||||
"extra": "base",
|
||||
"success": false,
|
||||
"resolver_succeeded": false,
|
||||
"identity": [],
|
||||
"errors": [],
|
||||
"diagnostics": " × No solution found when resolving dependencies:\n ╰─▶ Because onnxruntime{(platform_machine != 'AMD64' and platform_machine\n != 'amd64' and platform_machine != 'x86_64') or sys_platform !=\n 'win32'}>=1.20.1,<=1.23.2 has no wheels with a matching Python\n ABI tag (e.g., `cp314`) and only the following versions of\n onnxruntime{(platform_machine != 'AMD64' and platform_machine !=\n 'amd64' and platform_machine != 'x86_64') or sys_platform != 'win32'}\n are available:\n onnxruntime{(platform_machine != 'AMD64' and platform_machine\n != 'amd64' and platform_machine != 'x86_64') or sys_platform !=\n 'win32'}<=1.23.2\n onnxruntime{(platform_machine != 'AMD64' and platform_machine\n != 'amd64' and platform_machine != 'x86_64') or sys_platform !=\n 'win32'}>=1.24.1\n we can conclude that onnxruntime{(platform_machine != 'AMD64' and\n platform_machine != 'amd64' and platform_machine != 'x86_64') or\n sys_platform != 'win32'}>=1.20.1,<1.24.1 cannot be used.\n And because onnxruntime{(platform_machine != 'AMD64' and\n platform_machine != 'amd64' and platform_machine != 'x86_64') or\n sys_platform != 'win32'}>=1.24.1 has no wheels with a matching\n platform tag (e.g., `macosx_14_0_x86_64`) and mineru==4.0.12 depends on\n onnxruntime{(platform_machine != 'AMD64' and platform_machine != 'amd64'\n and platform_machine != 'x86_64') or sys_platform != 'win32'}>=1.20.1,\n we can conclude that mineru==4.0.12 cannot be used.\n And because only mineru==4.0.12 is available and you require mineru, we\n can conclude that your requirements are unsatisfiable.\n\nhint: You require CPython 3.14 (`cp314`), but we only found wheels for `onnxruntime` (v1.23.2) with the following Python ABI tags: `cp310`, `cp311`, `cp312`, `cp313`, `cp313t`\nhint: Wheels are available for `onnxruntime` (v1.31.0) on the following platforms: `manylinux_2_28_aarch64`, `manylinux_2_28_x86_64`, `macosx_14_0_arm64`, `win_amd64`, `win_arm64`\n"
|
||||
},
|
||||
{
|
||||
"platform": "macos",
|
||||
"python": "3.10",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.23.2"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos",
|
||||
"python": "3.11",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos",
|
||||
"python": "3.12",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos",
|
||||
"python": "3.13",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "macos",
|
||||
"python": "3.14",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime==1.31.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "windows",
|
||||
"python": "3.10",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime-directml==1.23.0"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "windows",
|
||||
"python": "3.11",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime-directml==1.24.4"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "windows",
|
||||
"python": "3.12",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime-directml==1.24.4"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "windows",
|
||||
"python": "3.13",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime-directml==1.24.4"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
},
|
||||
{
|
||||
"platform": "windows",
|
||||
"python": "3.14",
|
||||
"extra": "base",
|
||||
"success": true,
|
||||
"resolver_succeeded": true,
|
||||
"identity": [
|
||||
"mineru-magika==1.0.3",
|
||||
"onnxruntime-directml==1.24.4"
|
||||
],
|
||||
"errors": [],
|
||||
"diagnostics": ""
|
||||
}
|
||||
],
|
||||
"pypi_default_control": {
|
||||
"accepted": false,
|
||||
"selected": [
|
||||
"docvortex==0.5.16",
|
||||
"magika==1.0.3",
|
||||
"onnxruntime==1.31.0",
|
||||
"onnxruntime-directml==1.24.4"
|
||||
],
|
||||
"reason": "PyPI docvortex 0.5.16 still has old CPU ORT and upstream magika requirements; use pinned compatible source until a new compatible release and minimum-version update"
|
||||
},
|
||||
"raw_evidence_directory": "output\\onnx-dependency-acceptance-20261018",
|
||||
"source_install_used_no_deps": false
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
{
|
||||
"date": "2026-10-18",
|
||||
"threshold": 0.8,
|
||||
"label": "i7",
|
||||
"cpu": "12th Gen Intel(R) Core(TM) i7-12700",
|
||||
"gpu": "Intel(R) UHD Graphics 770",
|
||||
"requested_runs": 10,
|
||||
"successful_runs": 10,
|
||||
"cpu_selections": 10,
|
||||
"gpu_selections": 0,
|
||||
"errors": [],
|
||||
"validation_passed": true,
|
||||
"minimum_ratio": 1.541681982052094,
|
||||
"maximum_ratio": 1.8595392617371458,
|
||||
"median_ratio": 1.6057012217187245,
|
||||
"signature": [
|
||||
"5d6728644b7f9ba220b3072eb00b6d436e774f32f3d30cddd06f0017042f4375",
|
||||
"474fa06cbc4c20303081e85084b45f3a4f24190d1141e4bd3bf3f3feb432c078",
|
||||
"5435fd747c9e0efe15a96d0b378d5bd157e9492ed8fd80edf08f30d02fa24634",
|
||||
"1.24.4",
|
||||
4,
|
||||
1
|
||||
],
|
||||
"source_report_sha256": "2f10d1a93488413a44fa2c4012ebca6ccfd513ef593ad6ee50cfd7decad98f63",
|
||||
"protocol": "10 fresh workers; CPU first, GPU second; one warm-up and two timed batch6 runs per device; 4/1 threads; Intel rec metacommands off",
|
||||
"runs": [
|
||||
{
|
||||
"run": 1,
|
||||
"cpu": {
|
||||
"init_s": 0.08625900000333786,
|
||||
"first_s": 0.15661530010402203,
|
||||
"timed_s": [
|
||||
0.16551060043275356,
|
||||
0.1512667997740209
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.18019469967111945,
|
||||
"first_s": 0.37250009970739484,
|
||||
"timed_s": [
|
||||
0.24737749993801117,
|
||||
0.2507206997834146
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.3167774002067745,
|
||||
"gpu_elapsed": 0.49809819972142577,
|
||||
"ratio": 1.572391841704286,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.3538882001303136,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 2,
|
||||
"cpu": {
|
||||
"init_s": 0.11205330025404692,
|
||||
"first_s": 0.22100889962166548,
|
||||
"timed_s": [
|
||||
0.16745080007240176,
|
||||
0.15029080025851727
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.4032955998554826,
|
||||
"first_s": 0.3545626001432538,
|
||||
"timed_s": [
|
||||
0.24505490018054843,
|
||||
0.2448015999980271
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.317741600330919,
|
||||
"gpu_elapsed": 0.4898565001785755,
|
||||
"ratio": 1.541681982052094,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.664180700201541,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 3,
|
||||
"cpu": {
|
||||
"init_s": 0.07879989966750145,
|
||||
"first_s": 0.1900048996321857,
|
||||
"timed_s": [
|
||||
0.16707619978114963,
|
||||
0.1503589996136725
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.22637039981782436,
|
||||
"first_s": 0.3442545998841524,
|
||||
"timed_s": [
|
||||
0.24778660014271736,
|
||||
0.24555819993838668
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.3174351993948221,
|
||||
"gpu_elapsed": 0.49334480008110404,
|
||||
"ratio": 1.5541590882852523,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.4069735002703965,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 4,
|
||||
"cpu": {
|
||||
"init_s": 0.07987920008599758,
|
||||
"first_s": 0.17749949963763356,
|
||||
"timed_s": [
|
||||
0.15440709982067347,
|
||||
0.13619509991258383
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.18289580009877682,
|
||||
"first_s": 0.45124789979308844,
|
||||
"timed_s": [
|
||||
0.27159929974004626,
|
||||
0.2687869002111256
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.2906021997332573,
|
||||
"gpu_elapsed": 0.5403861999511719,
|
||||
"ratio": 1.8595392617371458,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.593133999966085,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 5,
|
||||
"cpu": {
|
||||
"init_s": 0.08565740007907152,
|
||||
"first_s": 0.16779650002717972,
|
||||
"timed_s": [
|
||||
0.1658973996527493,
|
||||
0.14534990023821592
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.16685859998688102,
|
||||
"first_s": 0.3460075999610126,
|
||||
"timed_s": [
|
||||
0.2489708000794053,
|
||||
0.24423420010134578
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.3112472998909652,
|
||||
"gpu_elapsed": 0.4932050001807511,
|
||||
"ratio": 1.5846081246440644,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.5043258001096547,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 6,
|
||||
"cpu": {
|
||||
"init_s": 0.08326179999858141,
|
||||
"first_s": 0.17670980002731085,
|
||||
"timed_s": [
|
||||
0.14763120003044605,
|
||||
0.1476646000519395
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.16335829999297857,
|
||||
"first_s": 0.3053096001967788,
|
||||
"timed_s": [
|
||||
0.24584809970110655,
|
||||
0.24588420009240508
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.29529580008238554,
|
||||
"gpu_elapsed": 0.49173229979351163,
|
||||
"ratio": 1.6652194161119855,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.276292699854821,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 7,
|
||||
"cpu": {
|
||||
"init_s": 0.08156939968466759,
|
||||
"first_s": 0.15990540012717247,
|
||||
"timed_s": [
|
||||
0.1556295999325812,
|
||||
0.1378607996739447
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.1639045001938939,
|
||||
"first_s": 0.3187303002923727,
|
||||
"timed_s": [
|
||||
0.24700330011546612,
|
||||
0.2433102000504732
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.2934903996065259,
|
||||
"gpu_elapsed": 0.49031350016593933,
|
||||
"ratio": 1.6706287525019166,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.2545465999282897,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 8,
|
||||
"cpu": {
|
||||
"init_s": 0.08818410011008382,
|
||||
"first_s": 0.15725370030850172,
|
||||
"timed_s": [
|
||||
0.146055500023067,
|
||||
0.16311899991706014
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.16608940018340945,
|
||||
"first_s": 0.30329359974712133,
|
||||
"timed_s": [
|
||||
0.2454309999011457,
|
||||
0.24593059998005629
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.30917449994012713,
|
||||
"gpu_elapsed": 0.491361599881202,
|
||||
"ratio": 1.5892694901305124,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.2385326996445656,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 9,
|
||||
"cpu": {
|
||||
"init_s": 0.08204400027170777,
|
||||
"first_s": 0.17115489998832345,
|
||||
"timed_s": [
|
||||
0.1495567001402378,
|
||||
0.14376969961449504
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.16184849990531802,
|
||||
"first_s": 0.3028835002332926,
|
||||
"timed_s": [
|
||||
0.2472366001456976,
|
||||
0.2436722000129521
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.29332639975473285,
|
||||
"gpu_elapsed": 0.4909088001586497,
|
||||
"ratio": 1.6735922868488036,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.2591197001747787,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"run": 10,
|
||||
"cpu": {
|
||||
"init_s": 0.0922516998834908,
|
||||
"first_s": 0.16064579971134663,
|
||||
"timed_s": [
|
||||
0.14966769982129335,
|
||||
0.15491799963638186
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.16616430040448904,
|
||||
"first_s": 0.3052006997168064,
|
||||
"timed_s": [
|
||||
0.2473430000245571,
|
||||
0.2467355001717806
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.3045856994576752,
|
||||
"gpu_elapsed": 0.4940785001963377,
|
||||
"ratio": 1.6221329533069366,
|
||||
"selected": "cpu",
|
||||
"worker_wall_s": 2.2529193996451795,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,899 @@
|
||||
{
|
||||
"threshold": 0.69639683662382,
|
||||
"lower": 0.5508244759786391,
|
||||
"upper": 0.841969197269001,
|
||||
"ratios": {
|
||||
"ultra5": [
|
||||
0.5222368732328241,
|
||||
0.5508244759786391,
|
||||
0.5295334203545369,
|
||||
0.4991853924531031,
|
||||
0.5114943474992194
|
||||
],
|
||||
"i7": [
|
||||
1.5809789058054133,
|
||||
1.5428846664602596,
|
||||
1.666585902822754,
|
||||
0.841969197269001,
|
||||
1.6714340091642932
|
||||
],
|
||||
"max395": [
|
||||
0.4016474016753725,
|
||||
0.3651728945628206,
|
||||
0.3828735159099384,
|
||||
0.3297758941471135,
|
||||
0.33356088170696885
|
||||
]
|
||||
},
|
||||
"signature": [
|
||||
"5d6728644b7f9ba220b3072eb00b6d436e774f32f3d30cddd06f0017042f4375",
|
||||
"474fa06cbc4c20303081e85084b45f3a4f24190d1141e4bd3bf3f3feb432c078",
|
||||
"5435fd747c9e0efe15a96d0b378d5bd157e9492ed8fd80edf08f30d02fa24634",
|
||||
"1.24.4",
|
||||
4,
|
||||
1
|
||||
],
|
||||
"status": "validated",
|
||||
"date": "2026-10-18",
|
||||
"frozen_bundle_sha256": "094760d45f0907964fad82a697ed54322c0fbcdaaa39f09304885bfa70d69781",
|
||||
"platforms": [
|
||||
{
|
||||
"label": "ultra5",
|
||||
"cpu": "Intel(R) Core(TM) Ultra 5 225H",
|
||||
"gpu": "Intel(R) Arc(TM) 130T GPU (16GB)",
|
||||
"vendor_id": 32902,
|
||||
"device_id": 32081,
|
||||
"uma": true,
|
||||
"display_drivers": [
|
||||
{
|
||||
"DriverDesc": "Intel(R) Arc(TM) 130T GPU (16GB)",
|
||||
"DriverVersion": "32.0.101.8991",
|
||||
"MatchingDeviceId": "PCI\\VEN_8086&DEV_7D51"
|
||||
},
|
||||
{
|
||||
"DriverDesc": "GameViewer Virtual Display Adapter",
|
||||
"DriverVersion": "15.6.5.199",
|
||||
"MatchingDeviceId": "Root\\GameViewerIddDriver"
|
||||
}
|
||||
],
|
||||
"selected": "gpu",
|
||||
"calibration": [
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.1245090999873355,
|
||||
"first_s": 0.1557560000801459,
|
||||
"timed_s": [
|
||||
0.1557519999332726,
|
||||
0.14047220000065863
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.168702200055122,
|
||||
"first_s": 0.196261000004597,
|
||||
"timed_s": [
|
||||
0.0804380999179557,
|
||||
0.07426110003143549
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.29622419993393123,
|
||||
"gpu_elapsed": 0.1546991999493912,
|
||||
"ratio": 0.5222368732328241,
|
||||
"worker_wall_s": 8.3898099999642,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07538569997996092,
|
||||
"first_s": 0.16537980001885444,
|
||||
"timed_s": [
|
||||
0.15101999999023974,
|
||||
0.14874009997583926
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.063421499915421,
|
||||
"first_s": 0.13587660004850477,
|
||||
"timed_s": [
|
||||
0.08014570001978427,
|
||||
0.08496949996333569
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.299760099966079,
|
||||
"gpu_elapsed": 0.16511519998311996,
|
||||
"ratio": 0.5508244759786391,
|
||||
"worker_wall_s": 8.340767999994569,
|
||||
"gpu_first": true,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.09954189998097718,
|
||||
"first_s": 0.15099960006773472,
|
||||
"timed_s": [
|
||||
0.14387050003279,
|
||||
0.1338656999869272
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.0616575999883935,
|
||||
"first_s": 0.1588244999293238,
|
||||
"timed_s": [
|
||||
0.0761950999731198,
|
||||
0.07087549997959286
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.2777362000197172,
|
||||
"gpu_elapsed": 0.14707059995271266,
|
||||
"ratio": 0.5295334203545369,
|
||||
"worker_wall_s": 8.172107200021856,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.06927199999336153,
|
||||
"first_s": 0.16087769996374846,
|
||||
"timed_s": [
|
||||
0.15035170002374798,
|
||||
0.14322520000860095
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.0834626000141725,
|
||||
"first_s": 0.12122359999921173,
|
||||
"timed_s": [
|
||||
0.07379920000676066,
|
||||
0.07275010005105287
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.29357690003234893,
|
||||
"gpu_elapsed": 0.14654930005781353,
|
||||
"ratio": 0.4991853924531031,
|
||||
"worker_wall_s": 8.232045500073582,
|
||||
"gpu_first": true,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.09695310005918145,
|
||||
"first_s": 0.1524447000119835,
|
||||
"timed_s": [
|
||||
0.1417236999841407,
|
||||
0.1385096999583766
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.022490600007586,
|
||||
"first_s": 0.15835319994948804,
|
||||
"timed_s": [
|
||||
0.07324170006904751,
|
||||
0.07009609998203814
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.2802333999425173,
|
||||
"gpu_elapsed": 0.14333780005108565,
|
||||
"ratio": 0.5114943474992194,
|
||||
"worker_wall_s": 8.233562199980952,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
}
|
||||
],
|
||||
"validation": [
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.10164109990000725,
|
||||
"first_s": 0.14734019991010427,
|
||||
"timed_s": [
|
||||
0.14145549992099404,
|
||||
0.13380010006949306
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.08250819996465,
|
||||
"first_s": 0.14972859993577003,
|
||||
"timed_s": [
|
||||
0.07329309999477118,
|
||||
0.07348050002474338
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.2752555999904871,
|
||||
"gpu_elapsed": 0.14677360001951456,
|
||||
"ratio": 0.5332265720464437,
|
||||
"worker_wall_s": 8.254710600012913,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.10452659998554736,
|
||||
"first_s": 0.15435500000603497,
|
||||
"timed_s": [
|
||||
0.14770820003468543,
|
||||
0.15267079998739064
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.437392899999395,
|
||||
"first_s": 0.21258459996897727,
|
||||
"timed_s": [
|
||||
0.08509210008196533,
|
||||
0.07600710005499423
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.30037900002207607,
|
||||
"gpu_elapsed": 0.16109920013695955,
|
||||
"ratio": 0.536319783091094,
|
||||
"worker_wall_s": 9.278964000055566,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.10005200002342463,
|
||||
"first_s": 0.16607539996039122,
|
||||
"timed_s": [
|
||||
0.15235289989504963,
|
||||
0.1595705000218004
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 4.254619500017725,
|
||||
"first_s": 0.16254559997469187,
|
||||
"timed_s": [
|
||||
0.07519969996064901,
|
||||
0.07101000007241964
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.31192339991685003,
|
||||
"gpu_elapsed": 0.14620970003306866,
|
||||
"ratio": 0.46873591424062455,
|
||||
"worker_wall_s": 8.56839440006297,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "i7",
|
||||
"cpu": "12th Gen Intel(R) Core(TM) i7-12700",
|
||||
"gpu": "Intel(R) UHD Graphics 770",
|
||||
"vendor_id": 32902,
|
||||
"device_id": 18048,
|
||||
"uma": true,
|
||||
"display_drivers": [
|
||||
{
|
||||
"DriverDesc": "GameViewer Virtual Display Adapter",
|
||||
"DriverVersion": "15.6.5.199",
|
||||
"MatchingDeviceId": "Root\\GameViewerIddDriver"
|
||||
},
|
||||
{
|
||||
"DriverDesc": "Intel(R) UHD Graphics 770",
|
||||
"DriverVersion": "32.0.101.6078",
|
||||
"MatchingDeviceId": "PCI\\VEN_8086&DEV_4680"
|
||||
},
|
||||
{
|
||||
"DriverDesc": "NVIDIA GeForce RTX 3060 Ti",
|
||||
"DriverVersion": "32.0.16.1060",
|
||||
"MatchingDeviceId": "pci\\ven_10de&dev_2489&subsys_c9761028"
|
||||
},
|
||||
{
|
||||
"DriverDesc": "Microsoft Remote Display Adapter",
|
||||
"DriverVersion": "10.0.22000.1042",
|
||||
"MatchingDeviceId": "RdpIdd_IndirectDisplay"
|
||||
},
|
||||
{
|
||||
"DriverDesc": "Microsoft Remote Display Adapter",
|
||||
"DriverVersion": "10.0.22000.1042",
|
||||
"MatchingDeviceId": "RdpIdd_IndirectDisplay"
|
||||
}
|
||||
],
|
||||
"selected": "cpu",
|
||||
"calibration": [
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.12509390013292432,
|
||||
"first_s": 0.16786340018734336,
|
||||
"timed_s": [
|
||||
0.16060509998351336,
|
||||
0.15400769980624318
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.20541869988664985,
|
||||
"first_s": 0.4193283002823591,
|
||||
"timed_s": [
|
||||
0.24820070015266538,
|
||||
0.2491954998113215
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.31461279978975654,
|
||||
"gpu_elapsed": 0.4973961999639869,
|
||||
"ratio": 1.5809789058054133,
|
||||
"worker_wall_s": 2.5029639997519553,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.06983949989080429,
|
||||
"first_s": 0.15956749999895692,
|
||||
"timed_s": [
|
||||
0.16224139975383878,
|
||||
0.15460029989480972
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.19752479996532202,
|
||||
"first_s": 0.36972759990021586,
|
||||
"timed_s": [
|
||||
0.24553100019693375,
|
||||
0.243319199886173
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.3168416996486485,
|
||||
"gpu_elapsed": 0.48885020008310676,
|
||||
"ratio": 1.5428846664602596,
|
||||
"worker_wall_s": 2.576211799867451,
|
||||
"gpu_first": true,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.08222260000184178,
|
||||
"first_s": 0.16638420009985566,
|
||||
"timed_s": [
|
||||
0.1508931997232139,
|
||||
0.1421357998624444
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.17086009960621595,
|
||||
"first_s": 0.30164070008322597,
|
||||
"timed_s": [
|
||||
0.24523840006440878,
|
||||
0.243119599763304
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.2930289995856583,
|
||||
"gpu_elapsed": 0.4883579998277128,
|
||||
"ratio": 1.666585902822754,
|
||||
"worker_wall_s": 2.242871499620378,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07191150030121207,
|
||||
"first_s": 0.17400800017639995,
|
||||
"timed_s": [
|
||||
0.16089960001409054,
|
||||
0.42469319980591536
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.17822950007393956,
|
||||
"first_s": 0.28638230031356215,
|
||||
"timed_s": [
|
||||
0.24899799982085824,
|
||||
0.24405309977009892
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.5855927998200059,
|
||||
"gpu_elapsed": 0.49305109959095716,
|
||||
"ratio": 0.841969197269001,
|
||||
"worker_wall_s": 2.648738599848002,
|
||||
"gpu_first": true,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07968680001795292,
|
||||
"first_s": 0.15783980023115873,
|
||||
"timed_s": [
|
||||
0.14695069985464215,
|
||||
0.1442038998939097
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.16532819997519255,
|
||||
"first_s": 0.2991844001226127,
|
||||
"timed_s": [
|
||||
0.24408889980986714,
|
||||
0.24255680013448
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.29115459974855185,
|
||||
"gpu_elapsed": 0.48664569994434714,
|
||||
"ratio": 1.6714340091642932,
|
||||
"worker_wall_s": 2.2790574999526143,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
}
|
||||
],
|
||||
"validation": [
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07856390019878745,
|
||||
"first_s": 0.1585060004144907,
|
||||
"timed_s": [
|
||||
0.155076100025326,
|
||||
0.13724670000374317
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.19285160023719072,
|
||||
"first_s": 0.3793517001904547,
|
||||
"timed_s": [
|
||||
0.2471511997282505,
|
||||
0.25015190010890365
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.2923228000290692,
|
||||
"gpu_elapsed": 0.49730309983715415,
|
||||
"ratio": 1.701212152414048,
|
||||
"worker_wall_s": 2.3880711998790503,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.0931462999433279,
|
||||
"first_s": 0.18240519985556602,
|
||||
"timed_s": [
|
||||
0.16190369985997677,
|
||||
0.14276310009881854
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.17211529985070229,
|
||||
"first_s": 0.33837390039116144,
|
||||
"timed_s": [
|
||||
0.24955720035359263,
|
||||
0.24976880010217428
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.3046667999587953,
|
||||
"gpu_elapsed": 0.4993260004557669,
|
||||
"ratio": 1.6389248862143768,
|
||||
"worker_wall_s": 2.320380700286478,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.13239890011027455,
|
||||
"first_s": 0.27217369992285967,
|
||||
"timed_s": [
|
||||
0.172813200391829,
|
||||
0.16785890003666282
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.18517510034143925,
|
||||
"first_s": 0.37796449987217784,
|
||||
"timed_s": [
|
||||
0.244259400293231,
|
||||
0.24512009974569082
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.34067210042849183,
|
||||
"gpu_elapsed": 0.48937950003892183,
|
||||
"ratio": 1.4365118230209877,
|
||||
"worker_wall_s": 2.5700910999439657,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "max395",
|
||||
"cpu": "AMD RYZEN AI MAX+ PRO 395 w/ Radeon 8060S",
|
||||
"gpu": "AMD Radeon(TM) 8060S Graphics",
|
||||
"vendor_id": 4098,
|
||||
"device_id": 5510,
|
||||
"uma": true,
|
||||
"display_drivers": [
|
||||
{
|
||||
"DriverDesc": "AMD Radeon(TM) 8060S Graphics",
|
||||
"DriverVersion": "32.0.31041.1004",
|
||||
"MatchingDeviceId": "PCI\\VEN_1002&DEV_1586&SUBSYS_8D01103C&REV_D1"
|
||||
},
|
||||
{
|
||||
"DriverDesc": "GameViewer Virtual Display Adapter",
|
||||
"DriverVersion": "15.6.5.199",
|
||||
"MatchingDeviceId": "Root\\GameViewerIddDriver"
|
||||
}
|
||||
],
|
||||
"selected": "gpu",
|
||||
"calibration": [
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07637480000266805,
|
||||
"first_s": 0.08280470001045614,
|
||||
"timed_s": [
|
||||
0.07512400002451614,
|
||||
0.07167690002825111
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.34370189998298883,
|
||||
"first_s": 0.11481369996909052,
|
||||
"timed_s": [
|
||||
0.03170540003338829,
|
||||
0.027256800036411732
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.14680090005276725,
|
||||
"gpu_elapsed": 0.05896220006980002,
|
||||
"ratio": 0.4016474016753725,
|
||||
"worker_wall_s": 1.8912332000327297,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.057475099980365485,
|
||||
"first_s": 0.09697800001595169,
|
||||
"timed_s": [
|
||||
0.09229130001040176,
|
||||
0.07979649998014793
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.33477970003150403,
|
||||
"first_s": 0.08208980003837496,
|
||||
"timed_s": [
|
||||
0.031072899990249425,
|
||||
0.03176890005124733
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.17208779999054968,
|
||||
"gpu_elapsed": 0.06284180004149675,
|
||||
"ratio": 0.3651728945628206,
|
||||
"worker_wall_s": 1.9066087000537664,
|
||||
"gpu_first": true,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07782819995190948,
|
||||
"first_s": 0.08145880000665784,
|
||||
"timed_s": [
|
||||
0.07784209999954328,
|
||||
0.07104310003342107
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.30829120002454147,
|
||||
"first_s": 0.10129029996460304,
|
||||
"timed_s": [
|
||||
0.030917699972633272,
|
||||
0.02608650003094226
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.14888520003296435,
|
||||
"gpu_elapsed": 0.057004200003575534,
|
||||
"ratio": 0.3828735159099384,
|
||||
"worker_wall_s": 1.7865404000040144,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.05741999996826053,
|
||||
"first_s": 0.08883979998063296,
|
||||
"timed_s": [
|
||||
0.07954020000761375,
|
||||
0.07562760001746938
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.33454459998756647,
|
||||
"first_s": 0.08503759995801374,
|
||||
"timed_s": [
|
||||
0.025020400003995746,
|
||||
0.02615019999211654
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.15516780002508312,
|
||||
"gpu_elapsed": 0.05117059999611229,
|
||||
"ratio": 0.3297758941471135,
|
||||
"worker_wall_s": 1.8132058000192046,
|
||||
"gpu_first": true,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.0747986999922432,
|
||||
"first_s": 0.0824259000364691,
|
||||
"timed_s": [
|
||||
0.08045999996829778,
|
||||
0.07203510002000257
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.31970799999544397,
|
||||
"first_s": 0.09892600000603124,
|
||||
"timed_s": [
|
||||
0.02595470001688227,
|
||||
0.02491169999120757
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.15249509998830035,
|
||||
"gpu_elapsed": 0.05086640000808984,
|
||||
"ratio": 0.33356088170696885,
|
||||
"worker_wall_s": 1.7935570999979973,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
}
|
||||
],
|
||||
"validation": [
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07893840002361685,
|
||||
"first_s": 0.08305060002021492,
|
||||
"timed_s": [
|
||||
0.07627840002533048,
|
||||
0.07216009998228401
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.32700849999673665,
|
||||
"first_s": 0.11254589998861775,
|
||||
"timed_s": [
|
||||
0.03159369999775663,
|
||||
0.026833700016140938
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.1484385000076145,
|
||||
"gpu_elapsed": 0.05842740001389757,
|
||||
"ratio": 0.39361351678237383,
|
||||
"worker_wall_s": 1.872372500016354,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07521159999305382,
|
||||
"first_s": 0.08401260001119226,
|
||||
"timed_s": [
|
||||
0.0768831999739632,
|
||||
0.07127350004157051
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.31867100001545623,
|
||||
"first_s": 0.10151509998831898,
|
||||
"timed_s": [
|
||||
0.0292708000051789,
|
||||
0.025711999973282218
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.14815670001553372,
|
||||
"gpu_elapsed": 0.05498279997846112,
|
||||
"ratio": 0.3711124773479455,
|
||||
"worker_wall_s": 1.8174999000038952,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
},
|
||||
{
|
||||
"cpu": {
|
||||
"init_s": 0.07662479998543859,
|
||||
"first_s": 0.0823513999930583,
|
||||
"timed_s": [
|
||||
0.08154410001588985,
|
||||
0.07119129999773577
|
||||
],
|
||||
"providers": [
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"gpu": {
|
||||
"init_s": 0.3092622999683954,
|
||||
"first_s": 0.10270039999159053,
|
||||
"timed_s": [
|
||||
0.029992799973115325,
|
||||
0.02598440001020208
|
||||
],
|
||||
"providers": [
|
||||
"DmlExecutionProvider",
|
||||
"CPUExecutionProvider"
|
||||
]
|
||||
},
|
||||
"cpu_elapsed": 0.15273540001362562,
|
||||
"gpu_elapsed": 0.055977199983317405,
|
||||
"ratio": 0.36649787788766486,
|
||||
"worker_wall_s": 1.8085962000186555,
|
||||
"gpu_first": false,
|
||||
"outputs_match": true,
|
||||
"rec_metacommands": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
## 1. 定位
|
||||
|
||||
ONNX 小模型新增 `model.onnx_device: auto | cpu | gpu`,环境变量为
|
||||
`MINERU_MODEL_ONNX_DEVICE`。首次实际使用时自动选择并保存,独显优先;
|
||||
集显测速、重新选择及 DirectML 厂商预设见 [ONNX 设备选择](onnx-device.md)。
|
||||
|
||||
配置体系定义 MinerU 如何在不同启动阶段得到最终行为。
|
||||
|
||||
MinerU 有两类配置:
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# ONNX 依赖验收(2026-10-18)
|
||||
|
||||
Windows x64 的完整安装验收通过,使用项目 `.venv`、Python 3.14.7 和 PowerShell 7。
|
||||
本轮使用已发布的 `mineru-magika` 与固定提交构建的 DocVortex,未使用 `--no-deps`、修改已安装元数据或依赖忽略规则。
|
||||
可复核结果见 [验收数据](benchmarks/onnx-dependency-20261018.json)。
|
||||
|
||||
## 安装来源与发行包身份
|
||||
|
||||
| 发行包 | 验收版本 / 来源 |
|
||||
|---|---|
|
||||
| MinerU | 本工作区构建的 `4.0.12` wheel;项目 `.venv` 安装为 editable |
|
||||
| DocVortex | `0.5.16`,提交 [`545570c4bffa2a668b7db03b5d3117e7a13faf8c`](https://github.com/myhloli/DocVortex/commit/545570c4bffa2a668b7db03b5d3117e7a13faf8c) |
|
||||
| mineru-magika | PyPI `1.0.3`,wheel SHA256 `1b67e05769f48c28d4d5dda0869ece4f4acce769ec7ab6ed169ed34ee5b58343` |
|
||||
| onnxruntime-directml | PyPI `1.24.4` |
|
||||
| mineru-vl-utils | PyPI `2.0.7` |
|
||||
|
||||
DocVortex 从干净的固定提交构建纯 Python wheel,`DOCVORTEX_BUILD_NATIVE=0`,与此前开发环境一致;
|
||||
本报告不包含 Rust 扩展构建验收。其他 wheel 的 SHA256 均记录在验收数据中。
|
||||
|
||||
迁移先卸载旧 `magika`,随后运行带完整依赖解析的:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\uv.exe pip install --python .venv/Scripts/python.exe --index-url https://pypi.org/simple -e ".[dev,test]" output/onnx-dependency-acceptance-20261018/dist/docvortex-0.5.16-py3-none-any.whl
|
||||
.\.venv\Scripts\uv.exe pip check --python .venv/Scripts/python.exe
|
||||
```
|
||||
|
||||
安装解析了 121 个包,安装后的 159 个发行包通过 `uv pip check`。
|
||||
发行包元数据与模块所有权检查确认:
|
||||
|
||||
- `onnxruntime` 模块只有 `onnxruntime-directml` 一个所有者;未安装 CPU 版 `onnxruntime`。
|
||||
- `magika` 模块只有 `mineru-magika` 一个所有者;未安装旧 `magika`。
|
||||
- `mineru-magika` 仅声明 `click` 依赖,没有任一种 ORT 依赖。
|
||||
- 实际 ORT 位于项目 `.venv/Lib/site-packages/onnxruntime/__init__.py`,版本 `1.24.4`,
|
||||
provider 为 `DmlExecutionProvider` 和 `CPUExecutionProvider`。
|
||||
|
||||
依赖检查脚本现同时拒绝两种 ORT 并存、旧 Magika 与新发行包并存,以及缺少新 Magika 的求解结果。
|
||||
Windows x64 平台条件补充小写 `amd64`,与 DocVortex 保持一致;条件仍不区分 Python 版本。
|
||||
|
||||
## 回归与真实执行
|
||||
|
||||
- MinerU 的 ONNX、配置、路由、协议、文件类型与依赖检查:**762 passed**,仅两个第三方弃用警告。
|
||||
- DocVortex 的安装元数据矩阵和真实文件 / 代码语言识别:**71 passed**。
|
||||
真实 Magika 会话使用 DirectML 发行包提供的 CPU provider,线程为 `4/1`;惰性导入检查通过。
|
||||
- 真实 HTML 分类、Flash HTML 原生解析和单页 PDF 原生文本解析通过。
|
||||
- Ultra 5 / Arc 130T 使用新依赖运行三个独立 rec 验证进程:
|
||||
|
||||
| 进程 | CPU 两次累计 / 秒 | GPU 两次累计 / 秒 | ratio | 阈值 0.8 的选择 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 0.259271 | 0.141918 | 0.547375 | GPU |
|
||||
| 2 | 0.261896 | 0.137173 | 0.523770 | GPU |
|
||||
| 3 | 0.262668 | 0.141822 | 0.539929 | GPU |
|
||||
|
||||
三次均实际加载 DML provider,CPU/GPU 识别结果一致,没有超时或异常。
|
||||
|
||||
使用新依赖在新进程中完成 demo1+demo2 的全部 19 页 basic + 强制 OCR。
|
||||
结果的完整 `pages` 数组与此前 CPU 基准完全相等,包括正文、公式、边界框、顺序和页面结构;
|
||||
layout、rec、MFR 使用 DML,其他模型使用 CPU。
|
||||
Markdown 文本也完全一致,统一为基准脚本的 Windows CRLF 写出方式后逐字节一致,SHA256 为
|
||||
`6fa409c5986b5207a4cadff8d106a063c977180758babe615fc5ce58ebd86b49`。
|
||||
本轮初始验收脚本使用 `write_bytes` 输出 LF,而基准使用 `write_text` 输出 CRLF,导致首次字节比较失败;
|
||||
已确认差异仅为换行,修正脚本,并保留 LF 原件、CRLF 对照及独立比较记录。
|
||||
|
||||
## 平台求解与 wheel 覆盖
|
||||
|
||||
使用三个真实 wheel 从头求解基础安装依赖,并核对 PyPI 的 Python 范围和目标 ABI / 平台 wheel。
|
||||
此处仅 Windows Python 3.14 执行了真实安装;其余格子是求解和发行文件检查,不代表在目标机器执行过安装或推理。
|
||||
|
||||
| 平台 | Python | 结果 | ORT / Magika 选择 |
|
||||
|---|---|---|---|
|
||||
| Windows x64 | 3.10 | 通过 | DirectML `1.23.0` + mineru-magika `1.0.3` |
|
||||
| Windows x64 | 3.11–3.14 | 全部通过 | DirectML `1.24.4` + mineru-magika `1.0.3` |
|
||||
| Linux x64 | 3.10–3.14 | 全部通过 | 仅 CPU ORT + mineru-magika `1.0.3` |
|
||||
| Apple Silicon / macOS 14 | 3.10–3.14 | 全部通过 | 仅 CPU ORT + mineru-magika `1.0.3` |
|
||||
| Intel Mac / macOS 14 | 3.10–3.13 | 求解通过;纯 wheel 检查失败 | 当前选出的 cryptography `50.0.2` 没有 Intel Mac wheel,源码构建未验收 |
|
||||
| Intel Mac / macOS 14 | 3.14 | 求解失败 | ORT `1.23.2` 无 CPython 3.14 wheel,较新 ORT 无 Intel Mac wheel |
|
||||
|
||||
上述 Intel Mac 限制单独记录,未改变用户要求保留的其他平台依赖约束。
|
||||
140 项 MinerU 平台 / Python / extra 标记测试及 DocVortex 安装元数据测试均确认 ORT 条件互斥。
|
||||
|
||||
并行检查还暴露了 Windows 下元数据缓存被另一进程读取时无法替换的竞争;检查脚本已改为复用已发布的完整缓存,
|
||||
随后 Windows 和 Linux 的矩阵重跑通过。
|
||||
|
||||
## PyPI 发布边界
|
||||
|
||||
截至本轮检查,PyPI 的 DocVortex 仍为旧元数据的 `0.5.16`;兼容提交的源码版本号也仍是 `0.5.16`。
|
||||
因此本轮通过的是**固定兼容源码的完整依赖验收**。
|
||||
额外以普通 PyPI 依赖求解作对照,仍选出旧 `magika==1.0.3`、CPU `onnxruntime==1.31.0`
|
||||
及 `onnxruntime-directml==1.24.4`,该组合明确判为不合格。
|
||||
|
||||
兼容版发布到 PyPI 后,需要提高 MinerU 的 DocVortex 最低版本,再验收不带源码覆盖的 PyPI 安装。
|
||||
在此之前,按 [安装说明](onnx-device.md#安装) 显式指定固定提交。
|
||||
|
||||
原始构建产物、安装前后清单、求解输出、测试日志及推理结果保存在
|
||||
`output/onnx-dependency-acceptance-20261018/`,未提交大型文档输出。
|
||||
@@ -0,0 +1,114 @@
|
||||
# ONNX 设备选择与 DirectML
|
||||
|
||||
`model.onnx_device` 控制 ONNX 小模型栈的 CPU / Windows DirectML 选择:
|
||||
|
||||
```yaml
|
||||
model:
|
||||
small_backend: onnx
|
||||
onnx_device: auto
|
||||
```
|
||||
|
||||
环境变量 `MINERU_MODEL_ONNX_DEVICE` 高于文件配置。`small_backend=auto` 仍按原有规则选择
|
||||
Torch 或 ONNX;此选项不强制切换模型栈,也不改变 Torch / VLM 的设备。
|
||||
|
||||
| 值 | 行为 |
|
||||
|---|---|
|
||||
| `cpu` | ONNX 小模型全部使用 CPU,不枚举显卡、不测速 |
|
||||
| `gpu` | 每个进程首次用模型时重新发现 GPU,独显优先,再选高性能排序中的集显,不测速 |
|
||||
| `auto` | 独显直接选 GPU;仅有集显时执行一次 rec 测速;没有硬件 GPU 时选 CPU |
|
||||
|
||||
Windows 使用 DXGI 高性能顺序、D3D12 UMA 属性和 LUID 匹配;适配器编号不写入配置。
|
||||
已保存的 `gpu` 使用 Windows 高性能排序及无设备创建的 D3D12 能力检查,避免逐张创建设备查询 UMA;
|
||||
`auto` 才需要 UMA 分类。旧系统缺少高性能排序接口时仍执行分类以保证独显优先。
|
||||
`gpu` 模式下移除显卡会在本次使用 CPU,仍保留配置中的 `gpu`。
|
||||
非 Windows、驱动探测错误、运行时缺少 DML 或 GPU 会话初始化失败时,日志会说明 CPU 回退原因。
|
||||
|
||||
首次实际使用模型时才选型。界面启动、导入 SDK、查询配置和模型列表不会运行测速。
|
||||
自动决策成功后原子更新有效配置文件中的 `model.onnx_device`,保留其余键;进程间通过文件锁协调。
|
||||
环境变量覆盖不写入文件;只读配置保留本进程选择。测速失败不保存为 CPU 性能优胜,同进程不重复尝试。
|
||||
通过环境变量强制 `auto` 时,每个新进程重新选择,并使用选择锁避免同时测速。
|
||||
显式或已保存的 `cpu` 不会因后来接入独显而改变。
|
||||
|
||||
要重新测量,将配置改回 `auto` 并重启应用。修改程序阈值也不主动覆盖已保存的选择。
|
||||
|
||||
## 模型预设
|
||||
|
||||
GPU 路线只将 layout、rec、MFR 交给 DML,检测、表格等其他 ONNX 模型保持 CPU。
|
||||
Torch 栈中的表格仍使用其原有 CPU/CUDA 策略。旧实验引擎和实验开关已经移除。
|
||||
|
||||
| 厂商 | layout | MFR | rec |
|
||||
|---|---|---|---|
|
||||
| Intel | 关 | 开 | 关 |
|
||||
| AMD | 开 | 开 | 开 |
|
||||
| NVIDIA | 开 | 开 | 关 |
|
||||
| 其他 | ORT 默认 | ORT 默认 | ORT 默认 |
|
||||
|
||||
表格中的开关指 metacommands,关闭对应 `disable_metacommands=true`。
|
||||
Intel layout 优先首轮响应;NVIDIA layout 采用后续补测结论。
|
||||
MFR 只有实际批量为 8 时使用 DML,1–7 张使用惰性创建、复用的 CPU 会话,不补齐。
|
||||
每个 DML 会话的 `run` 都串行化,不依赖宿主是否打开全局推理锁。
|
||||
|
||||
## 六图测速和阈值
|
||||
|
||||
图片和 manifest 位于包内 `resources/onnx_calibration`,包含原始中英数字句子、来源和 SHA256。
|
||||
重新生成工具 `scripts.make_onnx_calibration_samples` 需要显式提供字体;部署时直接读取固定 PNG,
|
||||
不需要系统字体。输入复用生产 rec 的 BGR、动态宽度和归一化逻辑;CPU/GPU 使用同一份 batch6。
|
||||
|
||||
每种设备预热一次,再计时两次。默认线程为 4/1;生产显式线程配置同时应用于测速。
|
||||
比较 `ratio = gpu_elapsed / cpu_elapsed`,当 `ratio <= ONNX_GPU_TIME_RATIO_THRESHOLD` 时选 GPU。
|
||||
阈值在 `mineru.model.runtime.onnx_device` 中集中维护。
|
||||
当前阈值按用户要求简化为 **0.8**,即 GPU 推理耗时至少比 CPU 低 20%。
|
||||
原三机校准推导值为 0.69639683662382,保留为历史参考;已有 24 次记录按 0.8 判断仍得到相同选择。
|
||||
详细数据见 [三机校准报告](onnx-selection-validation.md)。
|
||||
|
||||
下载和设备发现不计入 worker 预算。独立隐藏子进程的总超时为 15 秒,包含解释器启动、加载及推理;
|
||||
1–2 秒只是小样本推理目标。结果记录模型/输入校验值、ORT 版本与路径、provider、初始化、预热和正式计时。
|
||||
CPU/GPU 解码 token 必须一致,整会话回退 CPU 或计时无效都视为失败。
|
||||
|
||||
### 发布前校准
|
||||
|
||||
在 Ultra 5 / Arc 130T、i7-12700 / UHD 770、MAX395 / 8060S 上使用相同代码、权重、样本和 ORT:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe -m scripts.benchmark_onnx_selection devices
|
||||
.\.venv\Scripts\python.exe -m scripts.benchmark_onnx_selection run --label ultra5 --luid <集显LUID> --output output/calibration-ultra5
|
||||
```
|
||||
|
||||
另外两台使用 `--label i7` 和 `--label max395`。i7 必须在基准工具中明确选择 UHD 770,
|
||||
应用的正常独显优先规则仍会选择 RTX 3060 Ti。输出目录必须不存在。
|
||||
|
||||
每台运行五个新进程,交替 CPU/GPU 先后顺序。汇总命令只接受完整且协议一致的三份报告:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe -m scripts.benchmark_onnx_selection derive <ultra5报告> <i7报告> <max395报告> --output output/threshold.json
|
||||
```
|
||||
|
||||
取 Ultra 5 / MAX395 所有比值的最大值为 L,取 i7 最小比值与 1.0 的较小值为 U。
|
||||
只有 L < U 才给出中点 `(L + U) / 2` 作为统计建议;当前发布值按用户要求采用 0.8,
|
||||
不自动用统计建议覆盖正式常量。随后各平台以 `run --threshold <待验证值>` 和新目录运行三个
|
||||
独立验证进程,要求 Ultra 5、MAX395 全部选 GPU,i7 全部选 CPU。没有有效区间或验证失败时不发布阈值。
|
||||
阈值为 `None` 表示三机校准尚未完成;此时自动集显路径使用 CPU、保留 `auto`,显式 GPU 及独显仍可使用。
|
||||
|
||||
## 安装
|
||||
|
||||
Windows x64 的直接依赖为 `onnxruntime-directml>=1.23,<2`,其他平台保留 CPU 包约束,
|
||||
不按 Python 版本拆分条件。DirectML 包本身包含 CPU provider。
|
||||
|
||||
已有环境从 CPU 包迁移时,先卸载 `onnxruntime`,再安装 DirectML,避免两个发行包覆盖同一模块。
|
||||
安装后用 `import onnxruntime; print(onnxruntime.__version__, onnxruntime.get_available_providers())` 检查实际能力。
|
||||
|
||||
文件识别使用 `mineru-magika>=1.0.3,<1.1.0`,Python 导入名仍为 `magika`,由 DocVortex 声明依赖。
|
||||
迁移已有环境时须先卸载旧 `magika`,再安装新发行包,避免两个包覆盖同一模块。
|
||||
|
||||
2026-10-18 已使用 DocVortex 提交 `545570c4bffa2a668b7db03b5d3117e7a13faf8c` 完成
|
||||
Windows x64 的完整依赖安装、发行包身份、真实分类、rec 与文档解析验收。
|
||||
该提交的版本号仍为 `0.5.16`,当前 PyPI 同版本仍是旧依赖;因此验收使用固定提交构建的 wheel。
|
||||
在兼容版本发布到 PyPI 并更新最低版本前,源码安装应显式指定该提交:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\uv.exe pip install --python .venv/Scripts/python.exe -e . "docvortex @ git+https://github.com/myhloli/DocVortex.git@545570c4bffa2a668b7db03b5d3117e7a13faf8c"
|
||||
.\.venv\Scripts\uv.exe pip check --python .venv/Scripts/python.exe
|
||||
```
|
||||
|
||||
不能把两个 ORT 发行包或两个 Magika 发行包同时存在视为有效安装。
|
||||
版本、校验值、平台矩阵与实测结果见 [依赖验收报告](onnx-dependency-validation.md)。
|
||||
@@ -0,0 +1,107 @@
|
||||
# ONNX 六图选型:三机校准与验收
|
||||
|
||||
日期:2026-10-18。当前正式阈值:**0.8**,按用户要求简化,GPU 推理耗时需至少降低 20%。
|
||||
以下三机数据及中点计算保留为历史校准记录,当时使用的验收阈值为 0.69639683662382。
|
||||
|
||||
每台设备五个独立校准进程、三个独立验收进程,共 24 个新进程。
|
||||
每个进程的 CPU/GPU 各运行一次预热和两次计时,输入为相同的六张中英数字图片。
|
||||
使用 PP-OCRv6 small rec、DML 1.24.4、CPU 4/1 线程和生产预处理。
|
||||
CPU/GPU 解码结果全部一致,无失败或超时。
|
||||
|
||||
| 平台/集显 | 5 次校准 GPU/CPU 比值范围 | 3 次独立验收比值范围 | 验收选择 |
|
||||
|---|---:|---:|---|
|
||||
| Ultra 5 225H / Arc 130T | 0.499185–0.550824 | 0.468736–0.536320 | GPU,3/3 |
|
||||
| i7-12700 / UHD 770 | 0.841969–1.671434 | 1.436512–1.701212 | CPU,3/3 |
|
||||
| Ryzen AI MAX+ PRO 395 / 8060S | 0.329776–0.401647 | 0.366498–0.393614 | GPU,3/3 |
|
||||
|
||||
比值为 GPU 两次正式推理累计时间 / CPU 两次正式推理累计时间;越小表示 GPU 越快。
|
||||
会话加载、预热、解释器启动、文件读写和结果检查不进入这个比值。
|
||||
校准交替执行顺序,验收按生产流程固定 CPU 先、GPU 后。
|
||||
|
||||
## 历史校准阈值计算
|
||||
|
||||
- L = Ultra 5 与 MAX395 全部有效比值的最大值 = `0.5508244759786391`。
|
||||
- U = i7 最小有效比值与 1.0 的较小值 = `0.841969197269001`。
|
||||
- T = `(L + U) / 2` = `0.69639683662382`。
|
||||
|
||||
i7 校准中有一次比值为 0.841969,其余为 1.54–1.67;保留全部记录,没有剔除这一测量。
|
||||
三个后续独立验收均选择 CPU。阈值不依赖 CPU 名称或 GPU 型号匹配。
|
||||
正常设备发现另已确认 i7 主机的首选为 RTX 3060 Ti、普通 DXGI 索引 1;
|
||||
这台主机在应用正常 auto 模式下仍遵从独显优先,UHD 770 只由校准工具显式选择。
|
||||
|
||||
## 0.8 阈值:UHD 770 十次补验
|
||||
|
||||
用户将正式阈值简化为 0.8 后,在同一 i7-12700 / UHD 770 上补跑 10 个独立新进程。
|
||||
每次均按生产顺序 CPU 先、GPU 后,使用相同六图、权重、预处理、DML 1.24.4、4/1 线程及 Intel rec 预设。
|
||||
样本、输入和模型校验值与原三机报告完全一致。
|
||||
|
||||
**10/10 均选择 CPU,误选 GPU、超时和错误均为 0,CPU/GPU 解码结果全部一致。**
|
||||
GPU/CPU 耗时比最小 **1.541682**、最大 **1.859539**、中位数 **1.605701**,均高于 0.8。
|
||||
|
||||
| 次数 | CPU 两次合计(ms) | GPU 两次合计(ms) | GPU/CPU | 选择 |
|
||||
|---|---:|---:|---:|---|
|
||||
| 1 | 316.777 | 498.098 | 1.572392 | CPU |
|
||||
| 2 | 317.742 | 489.857 | 1.541682 | CPU |
|
||||
| 3 | 317.435 | 493.345 | 1.554159 | CPU |
|
||||
| 4 | 290.602 | 540.386 | 1.859539 | CPU |
|
||||
| 5 | 311.247 | 493.205 | 1.584608 | CPU |
|
||||
| 6 | 295.296 | 491.732 | 1.665219 | CPU |
|
||||
| 7 | 293.490 | 490.314 | 1.670629 | CPU |
|
||||
| 8 | 309.174 | 491.362 | 1.589269 | CPU |
|
||||
| 9 | 293.326 | 490.909 | 1.673592 | CPU |
|
||||
| 10 | 304.586 | 494.079 | 1.622133 | CPU |
|
||||
|
||||
原三机的 24 次历史记录按 0.8 重新判断也全部保持原选择:Ultra 5 / MAX395 为 GPU,i7/UHD 为 CPU。
|
||||
本次阈值更新相关 41 项测试通过;之前 593 项实现回归和 19 页一致性对照保留为历史验证记录。
|
||||
|
||||
[本次完整逐次计时](benchmarks/onnx-selection-uhd770-threshold08-windows-20261018.json)。
|
||||
原始补验报告 SHA256:`2f10d1a93488413a44fa2c4012ebca6ccfd513ef593ad6ee50cfd7decad98f63`。
|
||||
|
||||
## 延迟与复现
|
||||
|
||||
Ultra 5 的五次校准 worker 墙钟为 8.17–8.39 秒,MAX395 为 1.79–1.91 秒。
|
||||
这包含加载和第一次运行,因此不将“约 1–2 秒小样本推理目标”描述为所有机器的首次总延迟。
|
||||
完整的初始化、首次执行、正式计时及 worker 墙钟见机器可读报告。
|
||||
|
||||
三台使用相同的模型、manifest 和预处理输入校验值:
|
||||
|
||||
- 模型:`5435fd747c9e0efe15a96d0b378d5bd157e9492ed8fd80edf08f30d02fa24634`
|
||||
- Manifest:`5d6728644b7f9ba220b3072eb00b6d436e774f32f3d30cddd06f0017042f4375`
|
||||
- 输入:`474fa06cbc4c20303081e85084b45f3a4f24190d1141e4bd3bf3f3feb432c078`
|
||||
- 输入 shape:`[6, 3, 48, 1012]`
|
||||
|
||||
正式开关为 Intel rec 关闭 metacommands、AMD rec 开启。实际 provider 均经过检查。
|
||||
源码包和原始逐机报告保存在本轮 `output/onnx-selection-20261018/`,
|
||||
可提交的完整逐次计时见 [机器可读报告](benchmarks/onnx-selection-windows-20261018.json)。
|
||||
校准与重新执行方法见 [ONNX 设备选择](onnx-device.md)。
|
||||
|
||||
## 正式入口与结果一致性
|
||||
|
||||
初版实现相关回归共 **593 项通过**,覆盖设备策略、配置原子写入、会话并发、MFR 尾批、模型预加载、
|
||||
CLI、PDF 模型流程及资源生命周期;另有两项第三方弃用提示。wheel 构建成功,确认包含六张 PNG、
|
||||
manifest、正式阈值及互斥的平台运行时依赖。
|
||||
|
||||
本机使用正式解析入口,分别在新进程中配置 `onnx_device=cpu` 和 `gpu`,
|
||||
对 demo1+demo2 合并的 19 页 PDF 执行 basic + 强制 OCR:
|
||||
|
||||
- 两次均完成全部 19 页,Middle JSON 的 `pages` 数组完全一致,包含页面顺序、块结构、边界框、正文与公式。
|
||||
- Markdown 文件逐字节一致,SHA256 为 `6fa409c5986b5207a4cadff8d106a063c977180758babe615fc5ce58ebd86b49`。
|
||||
- GPU 路线实际使用 DML 的组件为 layout、rec、MFR;检测、表格和 MFR 不足 8 张的尾批使用 CPU。
|
||||
- 完整结果、会话 provider 和比较记录保存在本轮输出目录的 `document-*-verified/` 与 `document-comparison.json`。
|
||||
|
||||
额外使用隔离配置文件验证真实进程生命周期:首次 `auto` 测速后写入 `gpu`,下一新进程
|
||||
返回 `configured_gpu`,未重新测速,配置中的其他键保持不变。
|
||||
本机首次自动选择约 20.57 秒,包括 UMA 设备分类与约 8.11 秒的测速 worker;
|
||||
这不是单独的 rec 推理耗时,也不包含随后生产模型的加载。
|
||||
|
||||
发现逐张创建 D3D12 设备分类会使后续选择仍耗时约 12 秒后,已优化保存为 `gpu` 的路径:
|
||||
利用 Windows 高性能排序,并以空 `ppDevice` 检查 D3D12 能力,避免重复创建分类设备。
|
||||
本机该枚举约 0.11 秒,包含 ORT 导入和读取配置的完整选择约 0.33 秒,所选 LUID/编号不变。
|
||||
首次 `auto` 仍执行 UMA 分类;旧 Windows 缺少高性能排序接口时保留分类以确保独显优先。
|
||||
接口行为参见 [D3D12CreateDevice](https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-d3d12createdevice)。
|
||||
|
||||
安装依赖声明已经切换到 Windows x64 的 `onnxruntime-directml>=1.23,<2`。
|
||||
2026-10-18 已使用 `mineru-magika==1.0.3` 和 DocVortex 固定兼容提交完成 Windows x64
|
||||
完整依赖验收,`uv pip check` 通过,两个共享模块各自只有一个发行包所有者。
|
||||
DocVortex 的同版本 PyPI 元数据尚未更新;本次源码构建来源、回归与平台求解结果见
|
||||
[依赖验收报告](onnx-dependency-validation.md)。
|
||||
+19
-1
@@ -28,6 +28,7 @@ MINERU_ENV_PREFIX = "MINERU_"
|
||||
AutoBool = Literal["auto"] | bool
|
||||
ConfigSource = Literal["default", "file", "env"]
|
||||
ModelSource = Literal["auto", "huggingface", "modelscope", "local"]
|
||||
OnnxDevice = Literal["auto", "cpu", "gpu"]
|
||||
LogLevel = Literal["trace", "debug", "info", "warning", "error", "critical"]
|
||||
|
||||
_INTERPOLATION_RE = re.compile(r"\$\{(\w+)(?::-([^${}]*))?\}")
|
||||
@@ -268,7 +269,8 @@ def get_config_file_exists() -> bool:
|
||||
return _loaded_config.config_file_exists
|
||||
|
||||
|
||||
def update_config_file(patch: dict[str, Any]) -> None:
|
||||
def update_config_file(patch: dict[str, Any], *, only_if_onnx_auto: bool = False) -> bool:
|
||||
"""Atomically merge a patch, optionally comparing the current ONNX choice under the lock."""
|
||||
config_file = get_config_file_path()
|
||||
config_dir = os.path.dirname(config_file)
|
||||
if config_dir:
|
||||
@@ -283,6 +285,11 @@ def update_config_file(patch: dict[str, Any]) -> None:
|
||||
else:
|
||||
current = {}
|
||||
|
||||
if only_if_onnx_auto:
|
||||
model = current.get("model", {})
|
||||
if not isinstance(model, dict) or model.get("onnx_device", "auto") != "auto":
|
||||
return False
|
||||
|
||||
updated = _deep_merge(current, patch)
|
||||
fd, tmp_path = tempfile.mkstemp(prefix=".config.", suffix=".tmp", dir=config_dir or None)
|
||||
try:
|
||||
@@ -292,6 +299,14 @@ def update_config_file(patch: dict[str, Any]) -> None:
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
return True
|
||||
|
||||
|
||||
def read_onnx_device_from_file() -> OnnxDevice:
|
||||
"""Re-read only the device choice after acquiring the cross-process selection lock."""
|
||||
path = get_config_file_path()
|
||||
raw = _load_config(path) if os.path.isfile(path) else {}
|
||||
return ModelConfig(**raw.get("model", {})).onnx_device
|
||||
|
||||
|
||||
class UDSConfig(BaseModel):
|
||||
@@ -502,6 +517,7 @@ class ModelConfig(BaseModel):
|
||||
base_dir: str = _default_path("models")
|
||||
source: str = "auto"
|
||||
small_backend: Literal["auto", "onnx", "torch"] = "auto"
|
||||
onnx_device: OnnxDevice = "auto"
|
||||
vlm: VlmConfig = Field(default_factory=VlmConfig)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@@ -575,6 +591,8 @@ def PatchedConfig(**kwargs: Any) -> Config:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OnnxDevice",
|
||||
"read_onnx_device_from_file",
|
||||
"AutoBool",
|
||||
"ConfigSource",
|
||||
"LoadedConfig",
|
||||
|
||||
@@ -148,6 +148,8 @@ def show_cmd(
|
||||
f"model.source.source: {get_config_source('model.source')}",
|
||||
f"model.small_backend: {config.model.small_backend}",
|
||||
f"model.small_backend.source: {get_config_source('model.small_backend')}",
|
||||
f"model.onnx_device: {config.model.onnx_device}",
|
||||
f"model.onnx_device.source: {get_config_source('model.onnx_device')}",
|
||||
f"model.vlm.engine: {config.model.vlm.engine}",
|
||||
f"model.vlm.engine.source: {get_config_source('model.vlm.engine')}",
|
||||
t("Effective small backend: {backend}", backend=effective_small_backend),
|
||||
|
||||
@@ -66,7 +66,7 @@ class PPDocLayoutV2LayoutModelONNX(PPDocLayoutV2PostProcessor):
|
||||
self.imgsz = tuple(resize["target_size"])
|
||||
self.interpolation = int(resize["interp"])
|
||||
self.rescale_factor = 1.0 / 255.0
|
||||
self.session = ort_session(self.model_path, device, intra_op_num_threads)
|
||||
self.session = ort_session(self.model_path, device, intra_op_num_threads, model_kind="layout")
|
||||
self._input_names = [i.name for i in self.session.get_inputs()]
|
||||
self._output_name = self.session.get_outputs()[0].name
|
||||
logger.debug(
|
||||
|
||||
@@ -13,6 +13,7 @@ ONNX 模型输出 token IDs(int64),不是 logits——自回归循环已 b
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
@@ -51,7 +52,12 @@ class PPFormulaNetPlusMONNX:
|
||||
) -> None:
|
||||
"""初始化 CPU 公式模型,并复用 Torch Plus-M 的纯 Python 处理器。"""
|
||||
self.device = "cpu"
|
||||
self.session = ort_session(model_path, device, intra_op_num_threads)
|
||||
self.session = ort_session(model_path, device, intra_op_num_threads, model_kind="mfr")
|
||||
self.cpu_tail_enabled = "DmlExecutionProvider" in self.session.get_providers()
|
||||
self._cpu_tail_session = None
|
||||
self._cpu_tail_lock = threading.Lock()
|
||||
self._model_path = model_path
|
||||
self._intra_op_num_threads = intra_op_num_threads
|
||||
self.input_name = self.session.get_inputs()[0].name
|
||||
|
||||
# 从 yml 加载 tokenizer
|
||||
@@ -177,7 +183,13 @@ class PPFormulaNetPlusMONNX:
|
||||
step = min(CPU_BATCH_SIZE, max(1, batch_size))
|
||||
for start in range(0, len(valid_inputs), step):
|
||||
inputs = np.concatenate(valid_inputs[start : start + step], axis=0).astype(np.float32)
|
||||
preds = self.session.run(None, {self.input_name: inputs})[0]
|
||||
session = self.session
|
||||
if self.cpu_tail_enabled and inputs.shape[0] < CPU_BATCH_SIZE:
|
||||
with self._cpu_tail_lock:
|
||||
if self._cpu_tail_session is None:
|
||||
self._cpu_tail_session = ort_session(self._model_path, "cpu", self._intra_op_num_threads)
|
||||
session = self._cpu_tail_session
|
||||
preds = session.run(None, {self.input_name: inputs})[0]
|
||||
if preds.ndim != 2 or preds.shape[0] != inputs.shape[0]:
|
||||
raise ValueError("Formula ONNX output batch does not match input batch")
|
||||
for offset, tokens in enumerate(preds):
|
||||
|
||||
@@ -262,7 +262,7 @@ class TextRecognizerONNX:
|
||||
intra_op_num_threads: int = 0,
|
||||
) -> None:
|
||||
"""加载 CPU ONNX 会话并设置推理参数。"""
|
||||
self.session = ort_session(model_path, device, intra_op_num_threads)
|
||||
self.session = ort_session(model_path, device, intra_op_num_threads, model_kind="ocr_rec")
|
||||
self.input_name = self.session.get_inputs()[0].name
|
||||
|
||||
self.img_c, self.img_h, self.img_w = rec_image_shape
|
||||
|
||||
@@ -104,7 +104,9 @@ def table_cls_model_init(small_backend: str | None = None) -> PaddleTableClsMode
|
||||
"""初始化有线与无线表格类型分类模型。"""
|
||||
from ..table.cls.paddle_table_cls import PaddleTableClsModel
|
||||
|
||||
return PaddleTableClsModel(model_path=str(small_model_repo(small_backend).paddle_table_cls.ensure()))
|
||||
return PaddleTableClsModel(
|
||||
model_path=str(small_model_repo(small_backend).paddle_table_cls.ensure()), small_backend=small_backend
|
||||
)
|
||||
|
||||
|
||||
def wired_table_model_init(
|
||||
@@ -123,7 +125,9 @@ def wired_table_model_init(
|
||||
lang=lang,
|
||||
enable_merge_det_boxes=False,
|
||||
)
|
||||
table_model = UnetTableModel(ocr_engine, model_path=str(small_model_repo(small_backend).unet_structure.ensure()))
|
||||
table_model = UnetTableModel(
|
||||
ocr_engine, model_path=str(small_model_repo(small_backend).unet_structure.ensure()), small_backend=small_backend
|
||||
)
|
||||
return table_model
|
||||
|
||||
|
||||
@@ -143,7 +147,9 @@ def wireless_table_model_init(
|
||||
lang=lang,
|
||||
enable_merge_det_boxes=False,
|
||||
)
|
||||
table_model = PaddleTableModel(ocr_engine, model_path=str(small_model_repo(small_backend).slanet_plus.ensure()))
|
||||
table_model = PaddleTableModel(
|
||||
ocr_engine, model_path=str(small_model_repo(small_backend).slanet_plus.ensure()), small_backend=small_backend
|
||||
)
|
||||
return table_model
|
||||
|
||||
|
||||
@@ -233,6 +239,10 @@ class AtomModelSingleton:
|
||||
key = atom_model_name
|
||||
|
||||
key = (small_backend, device, key)
|
||||
if small_backend == "onnx":
|
||||
from .onnx_device import resolve_onnx_device
|
||||
|
||||
key = (*key, resolve_onnx_device())
|
||||
with self._lock:
|
||||
if key not in self._models:
|
||||
self._models[key] = atom_model_init(model_name=atom_model_name, **kwargs)
|
||||
@@ -339,7 +349,7 @@ class HybridLocalModelContextSingleton:
|
||||
"""全局缓存并复用 Hybrid 本地模型上下文。"""
|
||||
|
||||
_instance: HybridLocalModelContextSingleton | None = None
|
||||
_models: dict[tuple[str, str], HybridLocalModelContext] = {}
|
||||
_models: dict[tuple[object, ...], HybridLocalModelContext] = {}
|
||||
_lock: threading.RLock = LOCAL_MODEL_INIT_LOCK
|
||||
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> HybridLocalModelContextSingleton:
|
||||
@@ -355,7 +365,9 @@ class HybridLocalModelContextSingleton:
|
||||
"""按小模型后端与实际设备缓存上下文,避免切换配置后混用模型。"""
|
||||
small_backend = resolve_small_model_backend()
|
||||
device = "cpu" if small_backend == "onnx" else get_device()
|
||||
key = (small_backend, device)
|
||||
from ...config import config
|
||||
|
||||
key = (small_backend, device, config.model.onnx_device if small_backend == "onnx" else None)
|
||||
with self._lock:
|
||||
if key not in self._models:
|
||||
self._models[key] = HybridLocalModelContext(device=device, small_backend=small_backend)
|
||||
|
||||
@@ -3,11 +3,43 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import onnxruntime as ort
|
||||
from loguru import logger
|
||||
|
||||
from .onnx_device import OnnxSelection, confirm_gpu_session, metacommands_enabled, resolve_onnx_device
|
||||
from .windows_gpu import GpuAdapter
|
||||
|
||||
|
||||
class DirectMLSession:
|
||||
"""Serialize Run on a DML session, independently of host inference-lock settings."""
|
||||
|
||||
def __init__(self, session: ort.InferenceSession) -> None:
|
||||
self._session = session
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def run(self, output_names: list[str] | None, input_feed: dict[str, Any], run_options: Any = None) -> list[Any]:
|
||||
with self._lock:
|
||||
return self._session.run(output_names, input_feed, run_options)
|
||||
|
||||
def get_inputs(self) -> list[Any]:
|
||||
return self._session.get_inputs()
|
||||
|
||||
def get_outputs(self) -> list[Any]:
|
||||
return self._session.get_outputs()
|
||||
|
||||
def get_providers(self) -> list[str]:
|
||||
return self._session.get_providers()
|
||||
|
||||
def get_provider_options(self) -> dict[str, dict[str, str]]:
|
||||
return self._session.get_provider_options()
|
||||
|
||||
def end_profiling(self) -> str:
|
||||
return self._session.end_profiling()
|
||||
|
||||
|
||||
def get_op_num_threads(env_name: str) -> int:
|
||||
"""读取 ONNX 算子线程数,缺失或非法时返回负一。"""
|
||||
@@ -43,20 +75,90 @@ def ort_providers(device: str | None = None) -> list[tuple[str, dict[str, object
|
||||
return [("CPUExecutionProvider", {"arena_extend_strategy": "kSameAsRequested"})]
|
||||
|
||||
|
||||
def ort_session(model_path: str, device: str | None = None, intra_op_num_threads: int = 0) -> ort.InferenceSession:
|
||||
"""创建 CPU 会话;显式线程数优先,其次环境配置,默认使用 4/1 线程。"""
|
||||
def create_ort_session(
|
||||
model_path: str,
|
||||
*,
|
||||
adapter: GpuAdapter | None = None,
|
||||
model_kind: str = "",
|
||||
intra_op_num_threads: int = 0,
|
||||
inter_op_num_threads: int = 0,
|
||||
profile_prefix: str | None = None,
|
||||
) -> ort.InferenceSession | DirectMLSession:
|
||||
"""Create an explicit CPU/DML session without selection, downloads or config writes."""
|
||||
opts = ort.SessionOptions()
|
||||
opts.log_severity_level = 3
|
||||
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
configure_ort_threads(opts, intra_op_num_threads=intra_op_num_threads)
|
||||
return ort.InferenceSession(model_path, sess_options=opts, providers=ort_providers(device))
|
||||
configure_ort_threads(opts, intra_op_num_threads=intra_op_num_threads, inter_op_num_threads=inter_op_num_threads)
|
||||
providers = ort_providers()
|
||||
if adapter is not None:
|
||||
if "DmlExecutionProvider" not in ort.get_available_providers():
|
||||
raise RuntimeError("DmlExecutionProvider is unavailable")
|
||||
opts.enable_mem_pattern = False
|
||||
opts.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
options: dict[str, object] = {"device_id": adapter.index}
|
||||
enabled = metacommands_enabled(adapter.vendor, model_kind)
|
||||
if enabled is not None:
|
||||
options["disable_metacommands"] = "false" if enabled else "true"
|
||||
providers = [("DmlExecutionProvider", options), *providers]
|
||||
if profile_prefix is not None:
|
||||
opts.enable_profiling = True
|
||||
opts.profile_file_prefix = profile_prefix
|
||||
session = ort.InferenceSession(model_path, sess_options=opts, providers=providers)
|
||||
if adapter is not None:
|
||||
if session.get_providers()[0] != "DmlExecutionProvider":
|
||||
raise RuntimeError("Requested DML session fell back to CPU during initialization")
|
||||
session.disable_fallback()
|
||||
return DirectMLSession(session)
|
||||
return session
|
||||
|
||||
|
||||
def table_ort_session(model_path: str, *, sess_options: ort.SessionOptions | None = None) -> ort.InferenceSession:
|
||||
def ort_session(
|
||||
model_path: str,
|
||||
device: str | None = None,
|
||||
intra_op_num_threads: int = 0,
|
||||
*,
|
||||
model_kind: str = "",
|
||||
selection: OnnxSelection | None = None,
|
||||
) -> ort.InferenceSession | DirectMLSession:
|
||||
"""Use the process ONNX choice only for layout, rec and MFR; other models stay on CPU."""
|
||||
eligible = model_kind in {"layout", "ocr_rec", "mfr"}
|
||||
route = (selection or resolve_onnx_device(intra_op_num_threads=intra_op_num_threads)) if eligible else OnnxSelection()
|
||||
adapter = route.adapter if route.device == "gpu" else None
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
session = create_ort_session(
|
||||
model_path, adapter=adapter, model_kind=model_kind, intra_op_num_threads=intra_op_num_threads
|
||||
)
|
||||
except Exception as error:
|
||||
if adapter is None:
|
||||
raise
|
||||
logger.warning("ONNX {} DML initialization failed; using CPU: {}", model_kind, error)
|
||||
session = create_ort_session(model_path, intra_op_num_threads=intra_op_num_threads)
|
||||
else:
|
||||
if adapter is not None:
|
||||
confirm_gpu_session(route)
|
||||
logger.info(
|
||||
"ONNX session ready: kind={}, providers={}, gpu={}, metacommands={}, init_s={:.3f}",
|
||||
model_kind,
|
||||
session.get_providers(),
|
||||
adapter,
|
||||
metacommands_enabled(adapter.vendor, model_kind) if adapter else None,
|
||||
time.perf_counter() - started,
|
||||
)
|
||||
return session
|
||||
|
||||
|
||||
def table_ort_session(
|
||||
model_path: str, *, sess_options: ort.SessionOptions | None = None, small_backend: str | None = None
|
||||
) -> ort.InferenceSession:
|
||||
"""表格默认按可用 provider 选择设备;支持固定 CPU/CUDA,并明确记录 CUDA 失败回退。"""
|
||||
device = os.getenv("MINERU_TABLE_DEVICE", "auto").strip().lower()
|
||||
if device not in {"auto", "cpu", "cuda"}:
|
||||
raise ValueError("MINERU_TABLE_DEVICE must be 'auto', 'cpu' or 'cuda'")
|
||||
from .device import resolve_small_model_backend
|
||||
|
||||
if resolve_small_model_backend(small_backend) == "onnx":
|
||||
device = "cpu"
|
||||
opts = sess_options if sess_options is not None else ort.SessionOptions()
|
||||
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
opts.log_severity_level = 3
|
||||
@@ -107,4 +209,12 @@ def table_ort_session(model_path: str, *, sess_options: ort.SessionOptions | Non
|
||||
return session
|
||||
|
||||
|
||||
__all__ = ["configure_ort_threads", "get_op_num_threads", "ort_providers", "ort_session", "table_ort_session"]
|
||||
__all__ = [
|
||||
"DirectMLSession",
|
||||
"configure_ort_threads",
|
||||
"create_ort_session",
|
||||
"get_op_num_threads",
|
||||
"ort_providers",
|
||||
"ort_session",
|
||||
"table_ort_session",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Bounded rec calibration worker, shared by automatic selection and the release benchmark."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from .windows_gpu import GpuAdapter
|
||||
|
||||
CALIBRATION_TIMEOUT_SECONDS = 15
|
||||
|
||||
|
||||
def sample_directory() -> Path:
|
||||
"""Locate installed package data, independently of the working directory."""
|
||||
return Path(__file__).resolve().parents[2] / "resources/onnx_calibration"
|
||||
|
||||
|
||||
def prepare_input() -> tuple[Any, dict[str, Any]]:
|
||||
"""Check bundled PNG hashes and prepare the exact production batch once."""
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from ..ocr.image import resize_text_recognition_image
|
||||
|
||||
root = sample_directory()
|
||||
manifest_bytes = (root / "manifest.json").read_bytes()
|
||||
manifest = json.loads(manifest_bytes)
|
||||
samples = manifest["samples"]
|
||||
if len(samples) != 6:
|
||||
raise ValueError("Calibration requires exactly six samples")
|
||||
images = []
|
||||
for sample in samples:
|
||||
path = root / sample["file"]
|
||||
if path.parent != root or hashlib.sha256(path.read_bytes()).hexdigest() != sample["sha256"]:
|
||||
raise ValueError("Calibration sample checksum mismatch")
|
||||
with Image.open(path) as image:
|
||||
images.append(np.ascontiguousarray(np.asarray(image.convert("RGB"))[:, :, ::-1]))
|
||||
ratios = np.array([image.shape[1] / image.shape[0] for image in images])
|
||||
order = np.argsort(ratios)
|
||||
tensor = np.concatenate(
|
||||
[resize_text_recognition_image(images[index], float(ratios.max()), (3, 48, 320))[None] for index in order]
|
||||
).astype(np.float32)
|
||||
return tensor, {
|
||||
"manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
|
||||
"input_sha256": hashlib.sha256(tensor.tobytes()).hexdigest(),
|
||||
"shape": list(tensor.shape),
|
||||
"sample_order": order.tolist(),
|
||||
}
|
||||
|
||||
|
||||
def _tokens(outputs: Any) -> list[list[int]]:
|
||||
import numpy as np
|
||||
|
||||
if outputs.ndim != 3 or outputs.shape[0] != 6 or not np.isfinite(outputs).all():
|
||||
raise ValueError("Invalid rec calibration output")
|
||||
result = []
|
||||
for row in outputs.argmax(axis=-1):
|
||||
keep = np.ones(len(row), dtype=bool)
|
||||
keep[1:] = row[1:] != row[:-1]
|
||||
keep &= row != 0
|
||||
result.append(row[keep].tolist())
|
||||
return result
|
||||
|
||||
|
||||
def measure_rec(
|
||||
model_path: str,
|
||||
adapter: GpuAdapter,
|
||||
*,
|
||||
intra_op_num_threads: int = 0,
|
||||
inter_op_num_threads: int = 0,
|
||||
gpu_first: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""One warm-up and two timed runs per device; never consult the automatic selector."""
|
||||
import onnxruntime as ort
|
||||
from .onnx import configure_ort_threads, create_ort_session
|
||||
from .onnx_device import metacommands_enabled
|
||||
|
||||
tensor, evidence = prepare_input()
|
||||
options = ort.SessionOptions()
|
||||
configure_ort_threads(options, intra_op_num_threads=intra_op_num_threads, inter_op_num_threads=inter_op_num_threads)
|
||||
result: dict[str, Any] = {
|
||||
"schema_version": 1,
|
||||
"adapter": asdict(adapter),
|
||||
"input": evidence,
|
||||
"model_sha256": hashlib.sha256(Path(model_path).read_bytes()).hexdigest(),
|
||||
"ort_version": ort.__version__,
|
||||
"ort_path": ort.__file__,
|
||||
"python": sys.version,
|
||||
"intra_threads": options.intra_op_num_threads,
|
||||
"inter_threads": options.inter_op_num_threads,
|
||||
"rec_metacommands": metacommands_enabled(adapter.vendor, "ocr_rec"),
|
||||
"gpu_first": gpu_first,
|
||||
}
|
||||
decoded: dict[str, list[list[int]]] = {}
|
||||
for device in ("gpu", "cpu") if gpu_first else ("cpu", "gpu"):
|
||||
started = time.perf_counter()
|
||||
session = create_ort_session(
|
||||
model_path,
|
||||
adapter=adapter if device == "gpu" else None,
|
||||
model_kind="ocr_rec",
|
||||
intra_op_num_threads=options.intra_op_num_threads,
|
||||
inter_op_num_threads=options.inter_op_num_threads,
|
||||
)
|
||||
init_s = time.perf_counter() - started
|
||||
name = session.get_inputs()[0].name
|
||||
elapsed: list[float] = []
|
||||
reference = None
|
||||
for _ in range(3):
|
||||
started = time.perf_counter()
|
||||
outputs = session.run(None, {name: tensor})[0]
|
||||
elapsed.append(time.perf_counter() - started)
|
||||
tokens = _tokens(outputs)
|
||||
if reference is not None and tokens != reference:
|
||||
raise ValueError(f"{device} rec calibration output changed between runs")
|
||||
reference = tokens
|
||||
decoded[device] = reference
|
||||
result[device] = {"init_s": init_s, "first_s": elapsed[0], "timed_s": elapsed[1:], "providers": session.get_providers()}
|
||||
result[f"{device}_elapsed"] = sum(elapsed[1:])
|
||||
del session
|
||||
if decoded["cpu"] != decoded["gpu"]:
|
||||
raise ValueError("CPU and GPU rec calibration text differ")
|
||||
result["ratio"] = result["gpu_elapsed"] / result["cpu_elapsed"]
|
||||
result["outputs_match"] = True
|
||||
return result
|
||||
|
||||
|
||||
def run_calibration(
|
||||
model_path: str,
|
||||
adapter: GpuAdapter,
|
||||
*,
|
||||
intra_op_num_threads: int = 0,
|
||||
inter_op_num_threads: int = 0,
|
||||
gpu_first: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Run a fresh, hidden worker and kill it on timeout; failed measurements are never decisions."""
|
||||
request = {
|
||||
"model_path": model_path,
|
||||
"adapter": asdict(adapter),
|
||||
"intra_op_num_threads": intra_op_num_threads,
|
||||
"inter_op_num_threads": inter_op_num_threads,
|
||||
"gpu_first": gpu_first,
|
||||
}
|
||||
environment = {**os.environ, "PYTHONUTF8": "1", "MINERU_MODEL_ONNX_DEVICE": "cpu"}
|
||||
started = time.perf_counter()
|
||||
process = subprocess.run(
|
||||
[sys.executable, "-B", "-m", "mineru.model.runtime.onnx_calibration"],
|
||||
input=json.dumps(request),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=environment,
|
||||
timeout=CALIBRATION_TIMEOUT_SECONDS,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0,
|
||||
)
|
||||
if process.returncode:
|
||||
raise RuntimeError(f"ONNX calibration worker failed ({process.returncode}): {process.stderr[-1500:]}")
|
||||
result = json.loads(process.stdout)
|
||||
for key in ("cpu_elapsed", "gpu_elapsed"):
|
||||
if not isinstance(result.get(key), (float, int)) or not math.isfinite(result[key]) or result[key] <= 0:
|
||||
raise ValueError("Calibration worker returned invalid timings")
|
||||
if result.get("outputs_match") is not True or result["gpu"]["providers"][0] != "DmlExecutionProvider":
|
||||
raise ValueError("Calibration worker did not confirm valid DirectML results")
|
||||
result["worker_wall_s"] = time.perf_counter() - started
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Private JSON-over-stdin worker; no user files or global configuration are modified."""
|
||||
request = json.load(sys.stdin)
|
||||
adapter = GpuAdapter(**request.pop("adapter"))
|
||||
result = measure_rec(adapter=adapter, **request)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
|
||||
|
||||
__all__ = ["CALIBRATION_TIMEOUT_SECONDS", "measure_rec", "prepare_input", "run_calibration", "sample_directory"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Process-local ONNX selection, with a single persisted cpu/gpu preference."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import threading
|
||||
from typing import Literal
|
||||
|
||||
from filelock import FileLock
|
||||
from loguru import logger
|
||||
|
||||
from .windows_gpu import GpuAdapter, discover_gpus, preferred_gpu
|
||||
|
||||
# Require at least 20% lower GPU inference time; simplified to 0.8 at the user's request.
|
||||
# Evidence: docs/next/onnx-selection-validation.md. None disables unvalidated auto-iGPU choices.
|
||||
ONNX_GPU_TIME_RATIO_THRESHOLD: float | None = 0.8
|
||||
METACOMMANDS: dict[str, dict[str, bool]] = {
|
||||
"intel": {"layout": False, "mfr": True, "ocr_rec": False},
|
||||
"amd": {"layout": True, "mfr": True, "ocr_rec": True},
|
||||
"nvidia": {"layout": True, "mfr": True, "ocr_rec": False},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OnnxSelection:
|
||||
"""Immutable actual route. Adapter identity is valid only in this process."""
|
||||
|
||||
device: Literal["cpu", "gpu"] = "cpu"
|
||||
adapter: GpuAdapter | None = None
|
||||
reason: str = "cpu"
|
||||
|
||||
|
||||
_lock = threading.RLock()
|
||||
_selections: dict[tuple[int, str, str, str], OnnxSelection] = {}
|
||||
|
||||
|
||||
def metacommands_enabled(vendor: str, model_kind: str) -> bool | None:
|
||||
"""None leaves an unknown vendor on the ORT default."""
|
||||
return METACOMMANDS.get(vendor, {}).get(model_kind)
|
||||
|
||||
|
||||
def choose_gpu(cpu_elapsed: float, gpu_elapsed: float, threshold: float) -> bool:
|
||||
"""Compare valid elapsed times with the calibrated, conservatively bounded threshold."""
|
||||
import math
|
||||
|
||||
if not all(math.isfinite(value) and value > 0 for value in (cpu_elapsed, gpu_elapsed, threshold)):
|
||||
raise ValueError("Calibration times and threshold must be finite and positive")
|
||||
if threshold >= 1:
|
||||
raise ValueError("GPU threshold must be below 1")
|
||||
return gpu_elapsed / cpu_elapsed <= threshold
|
||||
|
||||
|
||||
def _select(requested: str, intra_op_num_threads: int) -> tuple[OnnxSelection, bool]:
|
||||
if requested == "cpu":
|
||||
return OnnxSelection(reason="configured_cpu"), False
|
||||
if sys.platform != "win32":
|
||||
if requested == "gpu":
|
||||
logger.warning("ONNX GPU selection requires Windows DirectML; using CPU")
|
||||
return OnnxSelection(reason="unsupported_platform"), False
|
||||
import onnxruntime as ort
|
||||
|
||||
if "DmlExecutionProvider" not in ort.get_available_providers():
|
||||
logger.warning("DirectML provider unavailable; using CPU without saving a performance decision")
|
||||
return OnnxSelection(reason="directml_unavailable"), False
|
||||
adapter = preferred_gpu(discover_gpus(classify=requested == "auto"))
|
||||
if adapter is None:
|
||||
return OnnxSelection(reason="no_hardware_gpu"), requested == "auto"
|
||||
if requested == "gpu" or adapter.uma is False:
|
||||
return OnnxSelection("gpu", adapter, "configured_gpu" if requested == "gpu" else "discrete_gpu"), False
|
||||
if ONNX_GPU_TIME_RATIO_THRESHOLD is None:
|
||||
logger.warning("ONNX integrated-GPU threshold has not completed calibration; using CPU without saving")
|
||||
return OnnxSelection(reason="calibration_pending"), False
|
||||
from ..registry import MINERU_4_MODELS_ONNX
|
||||
from .onnx_calibration import run_calibration
|
||||
|
||||
# Download follows the normal model lifecycle; it is outside the 15-second worker timeout.
|
||||
model = MINERU_4_MODELS_ONNX.ocr_rec.ensure()
|
||||
result = run_calibration(str(model), adapter, intra_op_num_threads=intra_op_num_threads)
|
||||
use_gpu = choose_gpu(result["cpu_elapsed"], result["gpu_elapsed"], ONNX_GPU_TIME_RATIO_THRESHOLD)
|
||||
logger.bind(onnx_calibration=result).info("ONNX rec calibration: {}", result)
|
||||
return OnnxSelection("gpu" if use_gpu else "cpu", adapter if use_gpu else None, "rec_calibration"), True
|
||||
|
||||
|
||||
def _select_and_save(requested: str, source: str, intra_op_num_threads: int) -> OnnxSelection:
|
||||
from ...config import read_onnx_device_from_file, update_config_file
|
||||
|
||||
if requested == "auto" and source != "env":
|
||||
requested = read_onnx_device_from_file()
|
||||
selection, completed = _select(requested, intra_op_num_threads)
|
||||
if completed and requested == "auto" and source != "env":
|
||||
try:
|
||||
update_config_file({"model": {"onnx_device": selection.device}}, only_if_onnx_auto=True)
|
||||
except OSError as error:
|
||||
logger.warning("ONNX choice is process-local because config could not be saved: {}", error)
|
||||
return selection
|
||||
|
||||
|
||||
def resolve_onnx_device(*, intra_op_num_threads: int = 0) -> OnnxSelection:
|
||||
"""Resolve at first model use; imports and diagnostic commands never call this function."""
|
||||
from ...config import config, get_config_file_path, get_config_source
|
||||
|
||||
requested = config.model.onnx_device
|
||||
source = get_config_source("model.onnx_device")
|
||||
path = get_config_file_path()
|
||||
key = (os.getpid(), path, requested, source)
|
||||
with _lock:
|
||||
if key in _selections:
|
||||
return _selections[key]
|
||||
try:
|
||||
if requested != "auto" or sys.platform != "win32":
|
||||
selection = _select_and_save(requested, source, intra_op_num_threads)
|
||||
else:
|
||||
try:
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
selection_lock = FileLock(f"{path}.onnx-selection.lock", timeout=30)
|
||||
selection_lock.acquire()
|
||||
except OSError:
|
||||
selection = _select_and_save(requested, source, intra_op_num_threads)
|
||||
else:
|
||||
try:
|
||||
selection = _select_and_save(requested, source, intra_op_num_threads)
|
||||
finally:
|
||||
selection_lock.release()
|
||||
except Exception as error:
|
||||
logger.warning("ONNX device selection failed; using CPU for this process: {}", error)
|
||||
selection = OnnxSelection(reason="selection_failed")
|
||||
_selections[key] = selection
|
||||
logger.info("ONNX device selected: {}", selection)
|
||||
return selection
|
||||
|
||||
|
||||
def confirm_gpu_session(selection: OnnxSelection) -> None:
|
||||
"""Persist a discrete-GPU auto choice only after an actual DML session succeeded."""
|
||||
if selection.reason != "discrete_gpu":
|
||||
return
|
||||
from ...config import config, get_config_source, update_config_file
|
||||
|
||||
if config.model.onnx_device == "auto" and get_config_source("model.onnx_device") != "env":
|
||||
try:
|
||||
update_config_file({"model": {"onnx_device": "gpu"}}, only_if_onnx_auto=True)
|
||||
except OSError as error:
|
||||
logger.warning("GPU is usable but config could not be saved: {}", error)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"METACOMMANDS",
|
||||
"ONNX_GPU_TIME_RATIO_THRESHOLD",
|
||||
"OnnxSelection",
|
||||
"choose_gpu",
|
||||
"confirm_gpu_session",
|
||||
"metacommands_enabled",
|
||||
"resolve_onnx_device",
|
||||
]
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Lazy Windows GPU discovery; DXGI indices are never persisted."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes as C
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GpuAdapter:
|
||||
"""One hardware adapter, with its ordinary DXGI index and preference rank."""
|
||||
|
||||
index: int
|
||||
luid: str
|
||||
name: str
|
||||
vendor_id: int
|
||||
device_id: int
|
||||
uma: bool | None
|
||||
preference: int
|
||||
|
||||
@property
|
||||
def vendor(self) -> str:
|
||||
return {0x8086: "intel", 0x1002: "amd", 0x10DE: "nvidia"}.get(self.vendor_id, "unknown")
|
||||
|
||||
|
||||
class _Guid(C.Structure):
|
||||
_fields_ = [("a", C.c_uint32), ("b", C.c_uint16), ("c", C.c_uint16), ("d", C.c_uint8 * 8)]
|
||||
|
||||
|
||||
class _Luid(C.Structure):
|
||||
_fields_ = [("low", C.c_uint32), ("high", C.c_int32)]
|
||||
|
||||
|
||||
class _Description(C.Structure):
|
||||
_fields_ = [
|
||||
("name", C.c_wchar * 128),
|
||||
("vendor", C.c_uint32),
|
||||
("device", C.c_uint32),
|
||||
("subsystem", C.c_uint32),
|
||||
("revision", C.c_uint32),
|
||||
("video_memory", C.c_size_t),
|
||||
("system_memory", C.c_size_t),
|
||||
("shared_memory", C.c_size_t),
|
||||
("luid", _Luid),
|
||||
("flags", C.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class _Architecture(C.Structure):
|
||||
_fields_ = [
|
||||
("node", C.c_uint32),
|
||||
("tile_based", C.c_int32),
|
||||
("uma", C.c_int32),
|
||||
("coherent", C.c_int32),
|
||||
("isolated", C.c_int32),
|
||||
]
|
||||
|
||||
|
||||
def _guid(value: str) -> _Guid:
|
||||
return _Guid.from_buffer_copy(uuid.UUID(value).bytes_le)
|
||||
|
||||
|
||||
def _method(pointer: C.c_void_p, slot: int, result: Any, *args: Any) -> Any:
|
||||
vtable = C.cast(pointer, C.POINTER(C.POINTER(C.c_void_p))).contents
|
||||
return C.WINFUNCTYPE(result, C.c_void_p, *args)(vtable[slot])
|
||||
|
||||
|
||||
def _release(pointer: C.c_void_p) -> None:
|
||||
if pointer:
|
||||
_method(pointer, 2, C.c_uint32)(pointer)
|
||||
|
||||
|
||||
def _check(hr: int) -> None:
|
||||
if hr < 0:
|
||||
raise OSError(f"Windows GPU discovery failed: HRESULT=0x{hr & 0xFFFFFFFF:08x}")
|
||||
|
||||
|
||||
def _description(adapter: C.c_void_p) -> _Description:
|
||||
desc = _Description()
|
||||
_check(_method(adapter, 10, C.c_int32, C.POINTER(_Description))(adapter, C.byref(desc)))
|
||||
return desc
|
||||
|
||||
|
||||
def _luid(desc: _Description) -> str:
|
||||
return f"{desc.luid.high & 0xFFFFFFFF:08x}:{desc.luid.low:08x}"
|
||||
|
||||
|
||||
def _architecture(adapter: C.c_void_p, create: Any, *, classify: bool = True) -> tuple[bool, bool | None]:
|
||||
device = C.c_void_p()
|
||||
iid = _guid("189819f1-1db6-4b57-be54-1821339b85f7")
|
||||
if not classify:
|
||||
# A null ppDevice checks D3D12 capability without creating a device (success is S_FALSE).
|
||||
return create(adapter, 0xB000, C.byref(iid), None) >= 0, None
|
||||
hr = create(adapter, 0xB000, C.byref(iid), C.byref(device))
|
||||
if hr < 0:
|
||||
return False, None
|
||||
try:
|
||||
info = _Architecture()
|
||||
check = _method(device, 13, C.c_int32, C.c_int32, C.c_void_p, C.c_uint32)
|
||||
# D3D12_FEATURE_ARCHITECTURE1, falling back to ARCHITECTURE on older drivers.
|
||||
hr = check(device, 16, C.byref(info), C.sizeof(info))
|
||||
if hr < 0:
|
||||
hr = check(device, 1, C.byref(info), C.sizeof(info) - C.sizeof(C.c_int32))
|
||||
return True, bool(info.uma) if hr >= 0 else None
|
||||
finally:
|
||||
_release(device)
|
||||
|
||||
|
||||
def discover_gpus(*, classify: bool = True) -> tuple[GpuAdapter, ...]:
|
||||
"""Discover D3D12 hardware; configured GPU mode only needs capability and DXGI performance order."""
|
||||
if sys.platform != "win32":
|
||||
return ()
|
||||
dxgi = C.WinDLL("dxgi", use_last_error=True)
|
||||
d3d = C.WinDLL("d3d12", use_last_error=True)
|
||||
create = dxgi.CreateDXGIFactory1
|
||||
create.argtypes = [C.POINTER(_Guid), C.POINTER(C.c_void_p)]
|
||||
create.restype = C.c_int32
|
||||
create_device = d3d.D3D12CreateDevice
|
||||
create_device.argtypes = [C.c_void_p, C.c_int32, C.POINTER(_Guid), C.POINTER(C.c_void_p)]
|
||||
create_device.restype = C.c_int32
|
||||
factory = C.c_void_p()
|
||||
modern = True
|
||||
iid = _guid("c1b6694f-ff09-44a9-b03c-77900a0a1d17")
|
||||
if create(C.byref(iid), C.byref(factory)) < 0:
|
||||
modern = False
|
||||
iid = _guid("770aae78-f26f-4dba-a829-253c83d1b387")
|
||||
_check(create(C.byref(iid), C.byref(factory)))
|
||||
try:
|
||||
ranks: dict[str, int] = {}
|
||||
if modern:
|
||||
enum_preference = _method(factory, 29, C.c_int32, C.c_uint32, C.c_int32, C.POINTER(_Guid), C.POINTER(C.c_void_p))
|
||||
adapter_iid = _guid("29038f61-3839-4626-91fd-086879011a05")
|
||||
for rank in range(64):
|
||||
adapter = C.c_void_p()
|
||||
hr = enum_preference(factory, rank, 2, C.byref(adapter_iid), C.byref(adapter))
|
||||
if hr & 0xFFFFFFFF == 0x887A0002:
|
||||
break
|
||||
_check(hr)
|
||||
try:
|
||||
ranks[_luid(_description(adapter))] = rank
|
||||
finally:
|
||||
_release(adapter)
|
||||
rows: list[GpuAdapter] = []
|
||||
enum = _method(factory, 12, C.c_int32, C.c_uint32, C.POINTER(C.c_void_p))
|
||||
for index in range(64):
|
||||
adapter = C.c_void_p()
|
||||
hr = enum(factory, index, C.byref(adapter))
|
||||
if hr & 0xFFFFFFFF == 0x887A0002:
|
||||
break
|
||||
_check(hr)
|
||||
try:
|
||||
desc = _description(adapter)
|
||||
if desc.flags & 2:
|
||||
continue
|
||||
# Old factories lack performance ordering, so classify to retain discrete-GPU priority.
|
||||
supported, uma = _architecture(adapter, create_device, classify=classify or not modern)
|
||||
if supported:
|
||||
luid = _luid(desc)
|
||||
rows.append(GpuAdapter(index, luid, desc.name, desc.vendor, desc.device, uma, ranks.get(luid, index)))
|
||||
finally:
|
||||
_release(adapter)
|
||||
return tuple(sorted(rows, key=lambda item: (item.uma is not False, item.preference, item.index)))
|
||||
finally:
|
||||
_release(factory)
|
||||
|
||||
|
||||
def preferred_gpu(adapters: tuple[GpuAdapter, ...]) -> GpuAdapter | None:
|
||||
"""Prefer a confirmed discrete GPU, then Windows performance preference."""
|
||||
return min(adapters, key=lambda item: (item.uma is not False, item.preference, item.index), default=None)
|
||||
|
||||
|
||||
__all__ = ["GpuAdapter", "discover_gpus", "preferred_gpu"]
|
||||
@@ -10,11 +10,13 @@ from ...runtime.onnx import table_ort_session
|
||||
|
||||
|
||||
class PaddleTableClsModel:
|
||||
def __init__(self, *, model_path: str | None = None) -> None:
|
||||
def __init__(self, *, model_path: str | None = None, small_backend: str | None = None) -> None:
|
||||
"""加载所选模型栈的表格分类文件,使用表格专用设备策略。"""
|
||||
from ...runtime.contracts import AtomicModelName
|
||||
|
||||
self.sess = table_ort_session(model_path or str(small_model_repo().paddle_table_cls.ensure()))
|
||||
self.sess = table_ort_session(
|
||||
model_path or str(small_model_repo(small_backend).paddle_table_cls.ensure()), small_backend=small_backend
|
||||
)
|
||||
self.less_length = 256
|
||||
self.cw, self.ch = 224, 224
|
||||
self.std = [0.229, 0.224, 0.225]
|
||||
|
||||
@@ -19,6 +19,7 @@ from .table_structure import TableStructurer
|
||||
|
||||
@dataclass
|
||||
class PaddleTableInput:
|
||||
small_backend: str | None = None
|
||||
model_type: Optional[str] = "slanet_plus"
|
||||
model_path: Union[str, Path, None, Dict[str, str]] = None
|
||||
use_cuda: bool = False
|
||||
@@ -150,10 +151,10 @@ def escape_html(input_string):
|
||||
|
||||
|
||||
class PaddleTableModel(object):
|
||||
def __init__(self, ocr_engine: Any, *, model_path: str | None = None) -> None:
|
||||
def __init__(self, ocr_engine: Any, *, model_path: str | None = None, small_backend: str | None = None) -> None:
|
||||
"""加载所选模型栈的无线表格资源并共享其 OCR 引擎。"""
|
||||
slanet_plus_model_path = model_path or str(small_model_repo().slanet_plus.ensure())
|
||||
input_args = PaddleTableInput(model_type="slanet_plus", model_path=slanet_plus_model_path)
|
||||
slanet_plus_model_path = model_path or str(small_model_repo(small_backend).slanet_plus.ensure())
|
||||
input_args = PaddleTableInput(model_type="slanet_plus", model_path=slanet_plus_model_path, small_backend=small_backend)
|
||||
self.table_model = PaddleTable(input_args)
|
||||
self.ocr_engine = ocr_engine
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class OrtInferSession:
|
||||
self.session = table_ort_session(
|
||||
model_path,
|
||||
sess_options=self._init_sess_opts(config),
|
||||
small_backend=config.get("small_backend"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -44,6 +44,7 @@ BLANK_CELL_REC_DROP_TEXTS = {
|
||||
@dataclass
|
||||
class WiredTableInput:
|
||||
model_path: str
|
||||
small_backend: str | None = None
|
||||
device: str = "cpu"
|
||||
|
||||
|
||||
@@ -261,10 +262,10 @@ def count_table_cells_physical(html_code):
|
||||
|
||||
|
||||
class UnetTableModel:
|
||||
def __init__(self, ocr_engine: Any, *, model_path: str | None = None) -> None:
|
||||
def __init__(self, ocr_engine: Any, *, model_path: str | None = None, small_backend: str | None = None) -> None:
|
||||
"""加载所选模型栈的有线表格资源并共享其 OCR 引擎。"""
|
||||
model_path = model_path or str(small_model_repo().unet_structure.ensure())
|
||||
wired_input_args = WiredTableInput(model_path=model_path)
|
||||
model_path = model_path or str(small_model_repo(small_backend).unet_structure.ensure())
|
||||
wired_input_args = WiredTableInput(model_path=model_path, small_backend=small_backend)
|
||||
self.wired_table_model = WiredTableRecognition(wired_input_args, ocr_engine)
|
||||
self.ocr_engine = ocr_engine
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class OrtInferSession:
|
||||
self.session = table_ort_session(
|
||||
model_path,
|
||||
sess_options=self._init_sess_opts(config),
|
||||
small_backend=config.get("small_backend"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source": "Original MinerU calibration sentences, rendered by scripts/make_onnx_calibration_samples.py",
|
||||
"font": "msyh.ttc",
|
||||
"font_sha256": "d79c55e68b1131eea0cc1c47be4f572d964f28c682e143db2ad09c1e4cb07a3f",
|
||||
"font_size": 32,
|
||||
"preprocessing": {
|
||||
"color": "BGR",
|
||||
"image_shape": [
|
||||
3,
|
||||
48,
|
||||
320
|
||||
],
|
||||
"batch": 6,
|
||||
"normalization": "production rec"
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"file": "line-1.png",
|
||||
"text": "文档解析与智能识别 2026",
|
||||
"sha256": "1e88f7e0a666403730db4bf96504ec07cde338c9633e71df407a1d65656613a0"
|
||||
},
|
||||
{
|
||||
"file": "line-2.png",
|
||||
"text": "文字、公式和表格转换为结构化内容,支持检索与分析。",
|
||||
"sha256": "2be97b59d7f7d034a9655622961f91b1686da8a6e2686ce2c034e3479231f7a6"
|
||||
},
|
||||
{
|
||||
"file": "line-3.png",
|
||||
"text": "Document recognition and structured content",
|
||||
"sha256": "c43bb9868537fa3e3b58bed9e30bfad0ba9405ad16773b6be7c2f60ce0f3d1a2"
|
||||
},
|
||||
{
|
||||
"file": "line-4.png",
|
||||
"text": "The quick brown fox jumps over the lazy dog. Page 123 of 456.",
|
||||
"sha256": "0100ddf30393caf6bebca3bb0d574ec09165952ca0352f4d429631b13cdb5b17"
|
||||
},
|
||||
{
|
||||
"file": "line-5.png",
|
||||
"text": "0123456789 9876543210 2026-10-18",
|
||||
"sha256": "ae5f25e5185ed9e1c3a8c757325fd01613393685def2ff31fe0559fa1d458689"
|
||||
},
|
||||
{
|
||||
"file": "line-6.png",
|
||||
"text": "Invoice 12345678: 3 x 128.50 = 385.50; Total 1,234.56",
|
||||
"sha256": "87f4e84ee377bffdd330d8da092446eb9aa57763b047746fa820f39e03c7d83e"
|
||||
}
|
||||
]
|
||||
}
|
||||
+2
-1
@@ -72,7 +72,8 @@ dependencies = [
|
||||
"typer>=0.9.0",
|
||||
"rich>=13.0.0",
|
||||
"jieba>=0.42.1",
|
||||
"onnxruntime>=1.20.1",
|
||||
"onnxruntime-directml>=1.23,<2 ; sys_platform == 'win32' and (platform_machine == 'AMD64' or platform_machine == 'amd64' or platform_machine == 'x86_64')",
|
||||
"onnxruntime>=1.20.1 ; sys_platform != 'win32' or (platform_machine != 'AMD64' and platform_machine != 'amd64' and platform_machine != 'x86_64')",
|
||||
"ftfy>=6.3.1,<7",
|
||||
"shapely>=2.0.7,<3",
|
||||
"pyclipper>=1.3.0,<2",
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Calibrate and independently validate the production six-line rec selection protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import asdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from mineru.model.runtime.onnx_calibration import run_calibration
|
||||
from mineru.model.runtime.onnx_device import choose_gpu
|
||||
from mineru.model.runtime.windows_gpu import discover_gpus, preferred_gpu
|
||||
|
||||
|
||||
def write_json(path: Path, data: dict[str, Any]) -> None:
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def environment() -> dict[str, Any]:
|
||||
"""Record host and power evidence outside the timed worker."""
|
||||
data: dict[str, Any] = {"host": platform.node(), "os": platform.platform(), "cpu": platform.processor()}
|
||||
if sys.platform == "win32":
|
||||
import winreg
|
||||
|
||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0") as key:
|
||||
data["cpu"] = winreg.QueryValueEx(key, "ProcessorNameString")[0].strip()
|
||||
power = subprocess.run(
|
||||
["powercfg", "/getactivescheme"], capture_output=True, timeout=10, creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
data["power_scheme"] = power.stdout.decode("utf-8", errors="replace").strip()
|
||||
drivers = []
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"
|
||||
) as parent:
|
||||
for i in range(winreg.QueryInfoKey(parent)[0]):
|
||||
name = winreg.EnumKey(parent, i)
|
||||
if not name.isdigit():
|
||||
continue
|
||||
with winreg.OpenKey(parent, name) as key:
|
||||
try:
|
||||
drivers.append(
|
||||
{
|
||||
field: winreg.QueryValueEx(key, field)[0]
|
||||
for field in ("DriverDesc", "DriverVersion", "MatchingDeviceId")
|
||||
}
|
||||
)
|
||||
except OSError:
|
||||
continue
|
||||
data["display_drivers"] = drivers
|
||||
return data
|
||||
|
||||
|
||||
def derive_threshold(reports: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""Choose the midpoint of the separating interval; reject mismatched or failed protocols."""
|
||||
by_label = {report["label"]: report for report in reports}
|
||||
if len(reports) != 3 or set(by_label) != {"ultra5", "i7", "max395"}:
|
||||
raise ValueError("Exactly one ultra5, i7 and max395 report is required")
|
||||
signatures = set()
|
||||
ratios: dict[str, list[float]] = {}
|
||||
for label, report in by_label.items():
|
||||
if report.get("errors") or len(report["runs"]) != 5 or report.get("threshold") is not None:
|
||||
raise ValueError(f"{label} needs five successful calibration processes")
|
||||
ratios[label] = []
|
||||
for run in report["runs"]:
|
||||
if not run.get("outputs_match") or run["gpu"]["providers"][0] != "DmlExecutionProvider":
|
||||
raise ValueError("Missing DirectML execution evidence")
|
||||
signatures.add(
|
||||
(
|
||||
run["input"]["manifest_sha256"],
|
||||
run["input"]["input_sha256"],
|
||||
run["model_sha256"],
|
||||
run["ort_version"],
|
||||
run["intra_threads"],
|
||||
run["inter_threads"],
|
||||
)
|
||||
)
|
||||
ratio = run["gpu_elapsed"] / run["cpu_elapsed"]
|
||||
choose_gpu(run["cpu_elapsed"], run["gpu_elapsed"], 0.5) # Validate finite positive timings only.
|
||||
ratios[label].append(ratio)
|
||||
if len(signatures) != 1:
|
||||
raise ValueError("Samples, weights, ORT or thread settings differ across calibration runs")
|
||||
lower = max(*ratios["ultra5"], *ratios["max395"])
|
||||
upper = min(*ratios["i7"], 1.0)
|
||||
if lower >= upper:
|
||||
raise ValueError(f"No separating interval: L={lower}, U={upper}; revise samples and rerun")
|
||||
return {
|
||||
"threshold": (lower + upper) / 2,
|
||||
"lower": lower,
|
||||
"upper": upper,
|
||||
"ratios": ratios,
|
||||
"signature": list(next(iter(signatures))),
|
||||
"status": "awaiting_independent_validation",
|
||||
}
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> None:
|
||||
adapters = discover_gpus()
|
||||
if args.luid:
|
||||
adapter = next((gpu for gpu in adapters if gpu.luid == args.luid), None)
|
||||
else:
|
||||
adapter = preferred_gpu(adapters)
|
||||
if adapter is None:
|
||||
raise ValueError("Requested hardware GPU was not found")
|
||||
if adapter.uma is not True:
|
||||
raise ValueError("Threshold calibration must explicitly target a confirmed integrated GPU")
|
||||
if args.model is None:
|
||||
from mineru.model.registry import MINERU_4_MODELS_ONNX
|
||||
|
||||
model = MINERU_4_MODELS_ONNX.ocr_rec.ensure()
|
||||
else:
|
||||
model = args.model.resolve(strict=True)
|
||||
# Refuse to overwrite a completed or partial experiment.
|
||||
args.output.mkdir(parents=True, exist_ok=False)
|
||||
report: dict[str, Any] = {
|
||||
"label": args.label,
|
||||
"environment": environment(),
|
||||
"adapter": asdict(adapter),
|
||||
"threshold": args.threshold,
|
||||
"runs": [],
|
||||
"errors": [],
|
||||
}
|
||||
target = args.output / "report.json"
|
||||
count = 5 if args.threshold is None else 3
|
||||
for index in range(count):
|
||||
try:
|
||||
result = run_calibration(
|
||||
str(model),
|
||||
adapter,
|
||||
intra_op_num_threads=4,
|
||||
inter_op_num_threads=1,
|
||||
gpu_first=args.threshold is None and index % 2 == 1,
|
||||
)
|
||||
if args.threshold is not None:
|
||||
result["selected"] = (
|
||||
"gpu" if choose_gpu(result["cpu_elapsed"], result["gpu_elapsed"], args.threshold) else "cpu"
|
||||
)
|
||||
report["runs"].append(result)
|
||||
print(
|
||||
f"{args.label} {index + 1}/{count}: ratio={result['ratio']:.6f}, wall={result['worker_wall_s']:.3f}s",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as error:
|
||||
report["errors"].append({"process": index, "error": str(error)})
|
||||
print(f"{args.label} {index + 1}/{count}: {error}", flush=True)
|
||||
write_json(target, report)
|
||||
if args.threshold is not None:
|
||||
expected = "cpu" if args.label == "i7" else "gpu"
|
||||
report["validation_passed"] = not report["errors"] and all(row["selected"] == expected for row in report["runs"])
|
||||
write_json(target, report)
|
||||
if report["errors"] or report.get("validation_passed") is False:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
commands.add_parser("devices")
|
||||
measure = commands.add_parser("run")
|
||||
measure.add_argument("--label", choices=("ultra5", "i7", "max395"), required=True)
|
||||
measure.add_argument("--luid", help="Benchmark-only adapter identity; use devices to find the iGPU")
|
||||
measure.add_argument("--model", type=Path)
|
||||
measure.add_argument("--output", type=Path, required=True)
|
||||
measure.add_argument("--threshold", type=float, help="Run three independent validation processes")
|
||||
derive = commands.add_parser("derive")
|
||||
derive.add_argument("reports", type=Path, nargs=3)
|
||||
derive.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if args.command == "devices":
|
||||
print(json.dumps([asdict(gpu) for gpu in discover_gpus()], ensure_ascii=False, indent=2))
|
||||
elif args.command == "run":
|
||||
run(args)
|
||||
else:
|
||||
if args.output.exists():
|
||||
raise FileExistsError(args.output)
|
||||
result = derive_threshold([json.loads(path.read_text(encoding="utf-8")) for path in args.reports])
|
||||
write_json(args.output, result)
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -66,7 +66,13 @@ def verify_wheels(resolution: str, platform: str, python: str, cache: Path, *, c
|
||||
with urllib.request.urlopen(f"https://pypi.org/pypi/{name}/{version}/json", timeout=30) as response:
|
||||
temporary = cached.with_suffix(f".{os.getpid()}.tmp")
|
||||
temporary.write_bytes(response.read())
|
||||
temporary.replace(cached)
|
||||
try:
|
||||
temporary.replace(cached)
|
||||
except PermissionError:
|
||||
# Another checker may have published the cache and opened it for reading on Windows.
|
||||
if not cached.is_file():
|
||||
raise
|
||||
temporary.unlink()
|
||||
data = json.loads(cached.read_text())
|
||||
requires_python = data["info"].get("requires_python")
|
||||
if requires_python and python not in SpecifierSet(requires_python):
|
||||
@@ -85,7 +91,13 @@ def check_backend_dependencies(selected: set[str], package: str, extra: str, pla
|
||||
checks = []
|
||||
if package == "mineru":
|
||||
# lxml 由 DocVortex 管理,不属于 MinerU 平台后端依赖。
|
||||
required = {"gradio", "onnxruntime", "mineru-llama-cpp"}
|
||||
runtime = "onnxruntime-directml" if platform == "windows" else "onnxruntime"
|
||||
required = {"gradio", runtime, "mineru-llama-cpp", "mineru-magika"}
|
||||
other_runtime = "onnxruntime" if platform == "windows" else "onnxruntime-directml"
|
||||
if other_runtime in selected:
|
||||
checks.append(f"conflicting {other_runtime} dependency on {platform}")
|
||||
if "magika" in selected:
|
||||
checks.append("upstream magika conflicts with the mineru-magika module namespace")
|
||||
if platform == "macos" or extra in {"torch", "full", "all"}:
|
||||
required.update({"torch", "torchvision", "transformers", "accelerate", "safetensors"})
|
||||
if extra in {"full", "all"} and platform in {"linux", "windows"}:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Build the six bundled, original text-line fixtures; no font is bundled at runtime."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
TEXTS = (
|
||||
"文档解析与智能识别 2026",
|
||||
"文字、公式和表格转换为结构化内容,支持检索与分析。",
|
||||
"Document recognition and structured content",
|
||||
"The quick brown fox jumps over the lazy dog. Page 123 of 456.",
|
||||
"0123456789 9876543210 2026-10-18",
|
||||
"Invoice 12345678: 3 x 128.50 = 385.50; Total 1,234.56",
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--font", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
destination = Path(__file__).resolve().parents[1] / "mineru/resources/onnx_calibration"
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
font = ImageFont.truetype(str(args.font), 32)
|
||||
rows = []
|
||||
for index, text in enumerate(TEXTS):
|
||||
bbox = font.getbbox(text)
|
||||
image = Image.new("RGB", (bbox[2] - bbox[0] + 12, bbox[3] - bbox[1] + 12), "white")
|
||||
ImageDraw.Draw(image).text((6 - bbox[0], 6 - bbox[1]), text, font=font, fill="black")
|
||||
path = destination / f"line-{index + 1}.png"
|
||||
image.save(path, optimize=True)
|
||||
rows.append({"file": path.name, "text": text, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"source": "Original MinerU calibration sentences, rendered by scripts/make_onnx_calibration_samples.py",
|
||||
"font": args.font.name,
|
||||
"font_sha256": hashlib.sha256(args.font.read_bytes()).hexdigest(),
|
||||
"font_size": 32,
|
||||
"preprocessing": {"color": "BGR", "image_shape": [3, 48, 320], "batch": 6, "normalization": "production rec"},
|
||||
"samples": rows,
|
||||
}
|
||||
(destination / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Unit tests must not benchmark real GPUs or persist choices in the developer's config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate_onnx_device_selection(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Individual policy tests explicitly override this CPU default with isolated files."""
|
||||
from mineru.config import config
|
||||
from mineru.model.runtime import onnx_device
|
||||
|
||||
monkeypatch.setattr(config.model, "onnx_device", "cpu")
|
||||
monkeypatch.setattr(onnx_device, "_selections", {})
|
||||
@@ -2,7 +2,21 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.check_transformers5_dependencies import verify_wheels
|
||||
import pytest
|
||||
|
||||
from scripts.check_transformers5_dependencies import check_backend_dependencies, verify_wheels
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform", ["windows", "linux", "macos-intel"])
|
||||
def test_runtime_and_magika_distribution_identity(platform: str) -> None:
|
||||
"""求解成功后仍拒绝共享模块的发行包冲突及旧 Magika 依赖。"""
|
||||
runtime = "onnxruntime-directml" if platform == "windows" else "onnxruntime"
|
||||
other_runtime = "onnxruntime" if platform == "windows" else "onnxruntime-directml"
|
||||
selected = {"gradio", runtime, "mineru-llama-cpp", "mineru-magika"}
|
||||
assert check_backend_dependencies(selected, "mineru", "base", platform) == []
|
||||
assert check_backend_dependencies(selected | {other_runtime}, "mineru", "base", platform)
|
||||
assert check_backend_dependencies(selected | {"magika"}, "mineru", "base", platform)
|
||||
assert check_backend_dependencies(selected - {"mineru-magika"}, "mineru", "base", platform)
|
||||
|
||||
|
||||
def test_python_metadata_is_checked_without_binary_wheel_option(tmp_path: Path) -> None:
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Production DML presets, explicit calibration sessions, serialization and MFR tails."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import threading
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from mineru.model.runtime import onnx
|
||||
from mineru.model.runtime.onnx_device import OnnxSelection
|
||||
from mineru.model.runtime.windows_gpu import GpuAdapter
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vendor", "enabled"),
|
||||
[(0x8086, [False, True, False]), (0x1002, [True, True, True]), (0x10DE, [True, True, False]), (0xFFFF, [None, None, None])],
|
||||
)
|
||||
def test_vendor_presets_and_actual_index(monkeypatch: pytest.MonkeyPatch, vendor: int, enabled: list[bool | None]) -> None:
|
||||
adapter = GpuAdapter(7, "luid", "gpu", vendor, 1, True, 0)
|
||||
monkeypatch.setattr(onnx.ort, "get_available_providers", lambda: ["DmlExecutionProvider", "CPUExecutionProvider"])
|
||||
native = SimpleNamespace(get_providers=lambda: ["DmlExecutionProvider", "CPUExecutionProvider"], disable_fallback=Mock())
|
||||
factory = Mock(return_value=native)
|
||||
monkeypatch.setattr(onnx.ort, "InferenceSession", factory)
|
||||
for kind, preset in zip(("layout", "mfr", "ocr_rec"), enabled):
|
||||
session = onnx.create_ort_session("test.onnx", adapter=adapter, model_kind=kind)
|
||||
assert isinstance(session, onnx.DirectMLSession)
|
||||
kwargs = factory.call_args.kwargs
|
||||
options = kwargs["providers"][0][1]
|
||||
assert options["device_id"] == 7
|
||||
assert options.get("disable_metacommands") == (None if preset is None else "false" if preset else "true")
|
||||
assert not kwargs["sess_options"].enable_mem_pattern
|
||||
assert kwargs["sess_options"].execution_mode == onnx.ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
assert native.disable_fallback.call_count == 3
|
||||
|
||||
|
||||
def test_cpu_models_do_not_select_gpu(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
select = Mock(side_effect=AssertionError("selection"))
|
||||
factory = Mock(return_value=SimpleNamespace(get_providers=lambda: ["CPUExecutionProvider"]))
|
||||
monkeypatch.setattr(onnx, "resolve_onnx_device", select)
|
||||
monkeypatch.setattr(onnx.ort, "InferenceSession", factory)
|
||||
for kind in ("", "ocr_det", "seal_det", "table", "mfr_tail_cpu"):
|
||||
onnx.ort_session("test.onnx", model_kind=kind)
|
||||
select.assert_not_called()
|
||||
assert all(call.kwargs["providers"] == onnx.ort_providers() for call in factory.call_args_list)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", ["unavailable", "exception", "silent"])
|
||||
def test_production_fallback_does_not_save_gpu(monkeypatch: pytest.MonkeyPatch, failure: str) -> None:
|
||||
adapter = GpuAdapter(1, "id", "gpu", 0x8086, 1, False, 0)
|
||||
cpu = SimpleNamespace(get_providers=lambda: ["CPUExecutionProvider"])
|
||||
monkeypatch.setattr(
|
||||
onnx.ort, "get_available_providers", lambda: [] if failure == "unavailable" else ["DmlExecutionProvider"]
|
||||
)
|
||||
factory = Mock(side_effect=[RuntimeError("gpu init"), cpu] if failure == "exception" else None, return_value=cpu)
|
||||
monkeypatch.setattr(onnx.ort, "InferenceSession", factory)
|
||||
save = Mock()
|
||||
monkeypatch.setattr(onnx, "confirm_gpu_session", save)
|
||||
assert onnx.ort_session("test.onnx", model_kind="layout", selection=OnnxSelection("gpu", adapter, "discrete_gpu")) is cpu
|
||||
save.assert_not_called()
|
||||
assert factory.call_args.kwargs["providers"] == onnx.ort_providers()
|
||||
|
||||
|
||||
def test_calibration_factory_fails_instead_of_falling_back(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(onnx.ort, "get_available_providers", lambda: ["DmlExecutionProvider"])
|
||||
monkeypatch.setattr(
|
||||
onnx.ort, "InferenceSession", Mock(return_value=SimpleNamespace(get_providers=lambda: ["CPUExecutionProvider"]))
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="fell back"):
|
||||
onnx.create_ort_session("test.onnx", adapter=GpuAdapter(0, "id", "gpu", 1, 1, True, 0))
|
||||
|
||||
|
||||
def test_run_is_serialized_per_session() -> None:
|
||||
active = 0
|
||||
maximum = 0
|
||||
lock = threading.Lock()
|
||||
|
||||
def run(names: object, feed: object, options: object) -> list[int]:
|
||||
nonlocal active, maximum
|
||||
with lock:
|
||||
active += 1
|
||||
maximum = max(active, maximum)
|
||||
time.sleep(0.01)
|
||||
with lock:
|
||||
active -= 1
|
||||
return [1]
|
||||
|
||||
session = onnx.DirectMLSession(SimpleNamespace(run=run))
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
assert list(pool.map(lambda _: session.run(None, {}), range(8))) == [[1]] * 8
|
||||
assert maximum == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "batch"), [(1, 8), (7, 8), (8, 8), (9, 8), (16, 8), (17, 8), (8, 4)])
|
||||
def test_mfr_only_actual_batch_eight_uses_gpu(monkeypatch: pytest.MonkeyPatch, count: int, batch: int) -> None:
|
||||
from mineru.model.mfr import pp_formulanet_plus_m_onnx as mfr
|
||||
|
||||
model = object.__new__(mfr.PPFormulaNetPlusMONNX)
|
||||
model.cpu_tail_enabled = True
|
||||
model._cpu_tail_session = None
|
||||
model._cpu_tail_lock = threading.Lock()
|
||||
model._model_path = "mfr.onnx"
|
||||
model._intra_op_num_threads = 4
|
||||
model.input_name = "image"
|
||||
gpu_calls, cpu_calls = [], []
|
||||
|
||||
def run(calls: list[int], feeds: dict[str, np.ndarray]) -> list[np.ndarray]:
|
||||
tensor = feeds["image"]
|
||||
calls.append(len(tensor))
|
||||
return [tensor[:, 0, 0, 0].astype(np.int64)[:, None]]
|
||||
|
||||
model.session = SimpleNamespace(run=lambda names, feeds: run(gpu_calls, feeds))
|
||||
factory = Mock(return_value=SimpleNamespace(run=lambda names, feeds: run(cpu_calls, feeds)))
|
||||
monkeypatch.setattr(mfr, "ort_session", factory)
|
||||
monkeypatch.setattr(model, "_preprocess", lambda crop: np.full((1, 1, 2, 2), crop[0, 0, 0]) if crop.size else None)
|
||||
monkeypatch.setattr(model, "_decode_tokens", lambda tokens: str(tokens[0]))
|
||||
crops = [np.full((value, value, 3), value, dtype=np.uint8) for value in range(count, 0, -1)]
|
||||
for _ in range(2):
|
||||
assert model._infer_batch(crops, batch_size=batch) == [str(value) for value in range(count, 0, -1)]
|
||||
assert all(size == 8 for size in gpu_calls)
|
||||
assert all(0 < size < 8 for size in cpu_calls)
|
||||
assert sum(gpu_calls + cpu_calls) == count * 2
|
||||
assert factory.call_count == (1 if cpu_calls else 0)
|
||||
@@ -69,7 +69,7 @@ doclib:
|
||||
assert data["doclib"]["data_dir"] == str(tmp_path / "data")
|
||||
assert data["doclib"]["tcp"]["enabled"] is True
|
||||
assert data["doclib"]["tcp"]["port"] == 18080
|
||||
assert data["doclib"]["sqlite"]["path"] == str(tmp_path / "data" / "doclib.db")
|
||||
assert Path(data["doclib"]["sqlite"]["path"]) == tmp_path / "data" / "doclib.db"
|
||||
|
||||
|
||||
def test_collect_env_overrides_uses_greedy_field_path_matching(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -327,13 +327,13 @@ def test_global_log_level_rejects_invalid_values(level: str) -> None:
|
||||
def test_default_doclib_data_dir_uses_doclib_directory() -> None:
|
||||
cfg = Config()
|
||||
|
||||
assert cfg.doclib.data_dir.endswith(".mineru/doclib")
|
||||
assert Path(cfg.doclib.data_dir).as_posix().endswith(".mineru/doclib")
|
||||
|
||||
|
||||
def test_default_model_config_uses_mineru_model_directory() -> None:
|
||||
cfg = Config()
|
||||
|
||||
assert cfg.model.base_dir.endswith(".mineru/models")
|
||||
assert Path(cfg.model.base_dir).as_posix().endswith(".mineru/models")
|
||||
assert cfg.model.source == "auto"
|
||||
|
||||
|
||||
@@ -483,12 +483,12 @@ def test_log_config_exposes_separate_log_paths() -> None:
|
||||
assert defaults.stderr_path is None
|
||||
assert defaults.parse_server_stdout_path is None
|
||||
assert defaults.parse_server_stderr_path is None
|
||||
assert defaults.resolved_app_path.endswith("logs/doclib.log")
|
||||
assert defaults.resolved_access_path.endswith("logs/doclib.access.log")
|
||||
assert defaults.resolved_stdout_path.endswith("logs/doclib.stdout.log")
|
||||
assert defaults.resolved_stderr_path.endswith("logs/doclib.stderr.log")
|
||||
assert defaults.resolved_parse_server_stdout_path.endswith("logs/doclib.parse-server.stdout.log")
|
||||
assert defaults.resolved_parse_server_stderr_path.endswith("logs/doclib.parse-server.stderr.log")
|
||||
assert Path(defaults.resolved_app_path).as_posix().endswith("logs/doclib.log")
|
||||
assert Path(defaults.resolved_access_path).as_posix().endswith("logs/doclib.access.log")
|
||||
assert Path(defaults.resolved_stdout_path).as_posix().endswith("logs/doclib.stdout.log")
|
||||
assert Path(defaults.resolved_stderr_path).as_posix().endswith("logs/doclib.stderr.log")
|
||||
assert Path(defaults.resolved_parse_server_stdout_path).as_posix().endswith("logs/doclib.parse-server.stdout.log")
|
||||
assert Path(defaults.resolved_parse_server_stderr_path).as_posix().endswith("logs/doclib.parse-server.stderr.log")
|
||||
|
||||
cfg = Config(
|
||||
doclib={
|
||||
@@ -511,12 +511,12 @@ def test_log_config_exposes_separate_log_paths() -> None:
|
||||
assert cfg.doclib.log.stderr_path == "/tmp/stderr.log"
|
||||
assert cfg.doclib.log.parse_server_stdout_path == "/tmp/parse-server.stdout.log"
|
||||
assert cfg.doclib.log.parse_server_stderr_path == "/tmp/parse-server.stderr.log"
|
||||
assert cfg.doclib.log.resolved_app_path == "/tmp/app.log"
|
||||
assert cfg.doclib.log.resolved_access_path == "/tmp/access.log"
|
||||
assert cfg.doclib.log.resolved_stdout_path == "/tmp/stdout.log"
|
||||
assert cfg.doclib.log.resolved_stderr_path == "/tmp/stderr.log"
|
||||
assert cfg.doclib.log.resolved_parse_server_stdout_path == "/tmp/parse-server.stdout.log"
|
||||
assert cfg.doclib.log.resolved_parse_server_stderr_path == "/tmp/parse-server.stderr.log"
|
||||
assert Path(cfg.doclib.log.resolved_app_path).as_posix() == "/tmp/app.log"
|
||||
assert Path(cfg.doclib.log.resolved_access_path).as_posix() == "/tmp/access.log"
|
||||
assert Path(cfg.doclib.log.resolved_stdout_path).as_posix() == "/tmp/stdout.log"
|
||||
assert Path(cfg.doclib.log.resolved_stderr_path).as_posix() == "/tmp/stderr.log"
|
||||
assert Path(cfg.doclib.log.resolved_parse_server_stdout_path).as_posix() == "/tmp/parse-server.stdout.log"
|
||||
assert Path(cfg.doclib.log.resolved_parse_server_stderr_path).as_posix() == "/tmp/parse-server.stderr.log"
|
||||
|
||||
|
||||
def test_log_config_dir_derives_unspecified_log_paths() -> None:
|
||||
@@ -526,12 +526,12 @@ def test_log_config_dir_derives_unspecified_log_paths() -> None:
|
||||
assert cfg.access_path is None
|
||||
assert cfg.stdout_path is None
|
||||
assert cfg.stderr_path == "/tmp/custom-stderr.log"
|
||||
assert cfg.resolved_app_path == "/tmp/mineru-logs/doclib.log"
|
||||
assert cfg.resolved_access_path == "/tmp/mineru-logs/doclib.access.log"
|
||||
assert cfg.resolved_stdout_path == "/tmp/mineru-logs/doclib.stdout.log"
|
||||
assert cfg.resolved_stderr_path == "/tmp/custom-stderr.log"
|
||||
assert cfg.resolved_parse_server_stdout_path == "/tmp/mineru-logs/doclib.parse-server.stdout.log"
|
||||
assert cfg.resolved_parse_server_stderr_path == "/tmp/mineru-logs/doclib.parse-server.stderr.log"
|
||||
assert Path(cfg.resolved_app_path).as_posix() == "/tmp/mineru-logs/doclib.log"
|
||||
assert Path(cfg.resolved_access_path).as_posix() == "/tmp/mineru-logs/doclib.access.log"
|
||||
assert Path(cfg.resolved_stdout_path).as_posix() == "/tmp/mineru-logs/doclib.stdout.log"
|
||||
assert Path(cfg.resolved_stderr_path).as_posix() == "/tmp/custom-stderr.log"
|
||||
assert Path(cfg.resolved_parse_server_stdout_path).as_posix() == "/tmp/mineru-logs/doclib.parse-server.stdout.log"
|
||||
assert Path(cfg.resolved_parse_server_stderr_path).as_posix() == "/tmp/mineru-logs/doclib.parse-server.stderr.log"
|
||||
|
||||
|
||||
def test_log_config_dir_override_derives_paths_in_deep_patches(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -548,24 +548,36 @@ def test_log_config_dir_override_derives_paths_in_deep_patches(monkeypatch: pyte
|
||||
assert env_cfg.doclib.log.stderr_path is None
|
||||
assert env_cfg.doclib.log.parse_server_stdout_path is None
|
||||
assert env_cfg.doclib.log.parse_server_stderr_path is None
|
||||
assert env_cfg.doclib.log.resolved_app_path == "/tmp/env-logs/doclib.log"
|
||||
assert env_cfg.doclib.log.resolved_access_path == "/tmp/env-logs/doclib.access.log"
|
||||
assert env_cfg.doclib.log.resolved_stdout_path == "/tmp/env-logs/doclib.stdout.log"
|
||||
assert env_cfg.doclib.log.resolved_stderr_path == "/tmp/env-logs/doclib.stderr.log"
|
||||
assert env_cfg.doclib.log.resolved_parse_server_stdout_path == "/tmp/env-logs/doclib.parse-server.stdout.log"
|
||||
assert env_cfg.doclib.log.resolved_parse_server_stderr_path == "/tmp/env-logs/doclib.parse-server.stderr.log"
|
||||
assert Path(env_cfg.doclib.log.resolved_app_path).as_posix() == "/tmp/env-logs/doclib.log"
|
||||
assert Path(env_cfg.doclib.log.resolved_access_path).as_posix() == "/tmp/env-logs/doclib.access.log"
|
||||
assert Path(env_cfg.doclib.log.resolved_stdout_path).as_posix() == "/tmp/env-logs/doclib.stdout.log"
|
||||
assert Path(env_cfg.doclib.log.resolved_stderr_path).as_posix() == "/tmp/env-logs/doclib.stderr.log"
|
||||
assert (
|
||||
Path(env_cfg.doclib.log.resolved_parse_server_stdout_path).as_posix()
|
||||
== "/tmp/env-logs/doclib.parse-server.stdout.log"
|
||||
)
|
||||
assert (
|
||||
Path(env_cfg.doclib.log.resolved_parse_server_stderr_path).as_posix()
|
||||
== "/tmp/env-logs/doclib.parse-server.stderr.log"
|
||||
)
|
||||
assert patched_cfg.doclib.log.app_path is None
|
||||
assert patched_cfg.doclib.log.access_path is None
|
||||
assert patched_cfg.doclib.log.stdout_path is None
|
||||
assert patched_cfg.doclib.log.stderr_path is None
|
||||
assert patched_cfg.doclib.log.parse_server_stdout_path is None
|
||||
assert patched_cfg.doclib.log.parse_server_stderr_path is None
|
||||
assert patched_cfg.doclib.log.resolved_app_path == "/tmp/patched-logs/doclib.log"
|
||||
assert patched_cfg.doclib.log.resolved_access_path == "/tmp/patched-logs/doclib.access.log"
|
||||
assert patched_cfg.doclib.log.resolved_stdout_path == "/tmp/patched-logs/doclib.stdout.log"
|
||||
assert patched_cfg.doclib.log.resolved_stderr_path == "/tmp/patched-logs/doclib.stderr.log"
|
||||
assert patched_cfg.doclib.log.resolved_parse_server_stdout_path == "/tmp/patched-logs/doclib.parse-server.stdout.log"
|
||||
assert patched_cfg.doclib.log.resolved_parse_server_stderr_path == "/tmp/patched-logs/doclib.parse-server.stderr.log"
|
||||
assert Path(patched_cfg.doclib.log.resolved_app_path).as_posix() == "/tmp/patched-logs/doclib.log"
|
||||
assert Path(patched_cfg.doclib.log.resolved_access_path).as_posix() == "/tmp/patched-logs/doclib.access.log"
|
||||
assert Path(patched_cfg.doclib.log.resolved_stdout_path).as_posix() == "/tmp/patched-logs/doclib.stdout.log"
|
||||
assert Path(patched_cfg.doclib.log.resolved_stderr_path).as_posix() == "/tmp/patched-logs/doclib.stderr.log"
|
||||
assert (
|
||||
Path(patched_cfg.doclib.log.resolved_parse_server_stdout_path).as_posix()
|
||||
== "/tmp/patched-logs/doclib.parse-server.stdout.log"
|
||||
)
|
||||
assert (
|
||||
Path(patched_cfg.doclib.log.resolved_parse_server_stderr_path).as_posix()
|
||||
== "/tmp/patched-logs/doclib.parse-server.stderr.log"
|
||||
)
|
||||
|
||||
|
||||
def test_interval_and_timeout_config_is_startup_config_not_runtime_kv() -> None:
|
||||
|
||||
@@ -205,10 +205,13 @@ def test_standard_dependency_errors_survive_startup_wrappers(
|
||||
assert doclib_error.value.param == "value"
|
||||
|
||||
|
||||
def _resolved_direct_dependencies(platform: str, machine: str, extra: str) -> set[str]:
|
||||
def _resolved_direct_dependencies(platform: str, machine: str, extra: str, python: str = "3.14") -> set[str]:
|
||||
"""按目标平台展开本项目的递归 extra,不使用宿主环境的标记值。"""
|
||||
project = tomllib.loads((Path(__file__).resolve().parents[2] / "pyproject.toml").read_text(encoding="utf-8"))["project"]
|
||||
environment = {**default_environment(), "sys_platform": platform, "platform_machine": machine}
|
||||
environment = {
|
||||
**default_environment(), "sys_platform": platform, "platform_machine": machine,
|
||||
"python_version": python, "python_full_version": python + ".0",
|
||||
}
|
||||
pending, visited, names = [extra], set(), set()
|
||||
while pending:
|
||||
current = pending.pop()
|
||||
@@ -228,13 +231,18 @@ def _resolved_direct_dependencies(platform: str, machine: str, extra: str) -> se
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"platform,machine", [("darwin", "arm64"), ("darwin", "x86_64"), ("linux", "x86_64"), ("win32", "AMD64")]
|
||||
"platform,machine",
|
||||
[("darwin", "arm64"), ("darwin", "x86_64"), ("linux", "x86_64"), ("win32", "AMD64"),
|
||||
("win32", "amd64"), ("win32", "x86_64"), ("win32", "ARM64")],
|
||||
)
|
||||
@pytest.mark.parametrize("extra", ["", "torch", "full", "all"])
|
||||
def test_package_markers(platform: str, machine: str, extra: str) -> None:
|
||||
@pytest.mark.parametrize("python", ["3.10", "3.11", "3.12", "3.13", "3.14"])
|
||||
def test_package_markers(platform: str, machine: str, extra: str, python: str) -> None:
|
||||
"""ARM 基础包引入完整 Torch 依赖,其他平台基础包保持 ONNX 与 llama。"""
|
||||
names = _resolved_direct_dependencies(platform, machine, extra)
|
||||
assert {"docvortex", "gradio", "onnxruntime", "mineru-llama-cpp"} <= names
|
||||
names = _resolved_direct_dependencies(platform, machine, extra, python)
|
||||
runtime = "onnxruntime-directml" if platform == "win32" and machine in {"AMD64", "amd64", "x86_64"} else "onnxruntime"
|
||||
assert {"docvortex", "gradio", runtime, "mineru-llama-cpp"} <= names
|
||||
assert not {"onnxruntime", "onnxruntime-directml"} <= names
|
||||
# lxml 由 DocVortex 声明,不在 MinerU 运行时 extras 中重复维护。
|
||||
assert "lxml" not in names
|
||||
expected_torch = (platform, machine) == ("darwin", "arm64") or extra in {"torch", "full", "all"}
|
||||
@@ -249,7 +257,7 @@ def test_package_markers(platform: str, machine: str, extra: str) -> None:
|
||||
|
||||
def test_real_resolution_policy_keeps_explicit_utils_mlx_separate() -> None:
|
||||
"""真实 wheel 检查允许 ARM 基础 Torch,但不把 utils 的显式 MLX 当成 MinerU 默认安装。"""
|
||||
names = _resolved_direct_dependencies("darwin", "arm64", "")
|
||||
names = _resolved_direct_dependencies("darwin", "arm64", "") | {"mineru-magika"}
|
||||
assert check_backend_dependencies(names, "mineru", "base", "macos") == []
|
||||
# 完整求解结果允许包含 DocVortex 带入的传递依赖。
|
||||
assert check_backend_dependencies(names | {"lxml"}, "mineru", "base", "macos") == []
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Validate the portable benchmark protocol and data-derived threshold selection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from mineru.model.runtime import onnx_calibration as calibration
|
||||
from mineru.model.runtime.windows_gpu import GpuAdapter
|
||||
from scripts.benchmark_onnx_selection import derive_threshold
|
||||
|
||||
|
||||
def gpu() -> GpuAdapter:
|
||||
return GpuAdapter(2, "id", "test", 0x8086, 1, True, 0)
|
||||
|
||||
|
||||
def test_installed_samples_are_fixed_and_complete() -> None:
|
||||
first, metadata = calibration.prepare_input()
|
||||
second, repeated = calibration.prepare_input()
|
||||
assert first.shape[:3] == (6, 3, 48)
|
||||
assert first.dtype == np.float32
|
||||
assert np.array_equal(first, second)
|
||||
assert metadata == repeated
|
||||
manifest = json.loads((calibration.sample_directory() / "manifest.json").read_text(encoding="utf-8"))
|
||||
text = "".join(row["text"] for row in manifest["samples"])
|
||||
assert any("\u4e00" <= character <= "\u9fff" for character in text)
|
||||
assert any(character.isascii() and character.isalpha() for character in text)
|
||||
assert any(character.isdigit() for character in text)
|
||||
|
||||
|
||||
def test_corrupt_sample_is_rejected(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
manifest = json.loads((calibration.sample_directory() / "manifest.json").read_text(encoding="utf-8"))
|
||||
(tmp_path / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
(tmp_path / manifest["samples"][0]["file"]).write_bytes(b"corrupt")
|
||||
monkeypatch.setattr(calibration, "sample_directory", lambda: tmp_path)
|
||||
with pytest.raises(ValueError, match="checksum"):
|
||||
calibration.prepare_input()
|
||||
|
||||
|
||||
def test_measurement_reuses_input_and_excludes_warmup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from mineru.model.runtime import onnx
|
||||
|
||||
tensor = np.zeros((6, 3, 48, 320), dtype=np.float32)
|
||||
feeds = []
|
||||
providers = []
|
||||
monkeypatch.setattr(calibration, "prepare_input", lambda: (tensor, {"shape": list(tensor.shape)}))
|
||||
ticks = iter([0, 4, 10, 110, 200, 202, 300, 303, 400, 408, 500, 600, 700, 701, 800, 802])
|
||||
monkeypatch.setattr(calibration.time, "perf_counter", lambda: next(ticks))
|
||||
model = tmp_path / "model.onnx"
|
||||
model.write_bytes(b"fake weights")
|
||||
|
||||
def create(path: str, **kwargs: object) -> SimpleNamespace:
|
||||
provider = "DmlExecutionProvider" if kwargs["adapter"] else "CPUExecutionProvider"
|
||||
providers.append(provider)
|
||||
|
||||
def run(names: object, feed: dict[str, np.ndarray]) -> list[np.ndarray]:
|
||||
feeds.append(feed["image"])
|
||||
return [np.ones((6, 3, 4), dtype=np.float32)]
|
||||
|
||||
return SimpleNamespace(get_inputs=lambda: [SimpleNamespace(name="image")], run=run, get_providers=lambda: [provider])
|
||||
|
||||
monkeypatch.setattr(onnx, "create_ort_session", create)
|
||||
result = calibration.measure_rec(str(model), gpu())
|
||||
assert providers == ["CPUExecutionProvider", "DmlExecutionProvider"]
|
||||
assert len(feeds) == 6 and all(feed is tensor for feed in feeds)
|
||||
assert result["cpu_elapsed"] == 5
|
||||
assert result["gpu_elapsed"] == 3
|
||||
assert result["cpu"]["first_s"] == result["gpu"]["first_s"] == 100
|
||||
assert result["cpu"]["init_s"] == 4 and result["gpu"]["init_s"] == 8
|
||||
|
||||
|
||||
def test_worker_timeout_is_bounded(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
runner = Mock(side_effect=subprocess.TimeoutExpired("worker", 15))
|
||||
monkeypatch.setattr(calibration.subprocess, "run", runner)
|
||||
with pytest.raises(subprocess.TimeoutExpired):
|
||||
calibration.run_calibration("rec.onnx", gpu())
|
||||
assert runner.call_args.kwargs["timeout"] == 15
|
||||
assert runner.call_args.kwargs["env"]["MINERU_MODEL_ONNX_DEVICE"] == "cpu"
|
||||
|
||||
|
||||
def test_worker_silent_fallback_is_not_a_measurement(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
output = {"cpu_elapsed": 1, "gpu_elapsed": 0.1, "outputs_match": True, "gpu": {"providers": ["CPUExecutionProvider"]}}
|
||||
monkeypatch.setattr(
|
||||
calibration.subprocess, "run", Mock(return_value=SimpleNamespace(returncode=0, stdout=json.dumps(output)))
|
||||
)
|
||||
with pytest.raises(ValueError, match="DirectML"):
|
||||
calibration.run_calibration("rec.onnx", gpu())
|
||||
|
||||
|
||||
def reports() -> list[dict]:
|
||||
result = []
|
||||
for label, ratios in (
|
||||
("ultra5", [0.5, 0.52, 0.55, 0.53, 0.51]),
|
||||
("i7", [1.2, 1.3, 1.1, 1.4, 1.3]),
|
||||
("max395", [0.3, 0.32, 0.29, 0.33, 0.31]),
|
||||
):
|
||||
runs = [
|
||||
{
|
||||
"cpu_elapsed": 1.0,
|
||||
"gpu_elapsed": ratio,
|
||||
"outputs_match": True,
|
||||
"gpu": {"providers": ["DmlExecutionProvider"]},
|
||||
"input": {"manifest_sha256": "samples", "input_sha256": "input"},
|
||||
"model_sha256": "model",
|
||||
"ort_version": "1.24.4",
|
||||
"intra_threads": 4,
|
||||
"inter_threads": 1,
|
||||
}
|
||||
for ratio in ratios
|
||||
]
|
||||
result.append({"label": label, "runs": runs, "errors": [], "threshold": None})
|
||||
return result
|
||||
|
||||
|
||||
def test_threshold_is_derived_from_all_five_runs() -> None:
|
||||
result = derive_threshold(reports())
|
||||
assert result["lower"] == 0.55
|
||||
assert result["upper"] == 1.0
|
||||
assert result["threshold"] == 0.775
|
||||
assert result["status"] == "awaiting_independent_validation"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", ["missing_host", "missing_run", "different_input", "different_ort", "failed", "overlap"])
|
||||
def test_invalid_calibration_set_is_rejected(failure: str) -> None:
|
||||
data = copy.deepcopy(reports())
|
||||
if failure == "missing_host":
|
||||
data.pop()
|
||||
elif failure == "missing_run":
|
||||
data[0]["runs"].pop()
|
||||
elif failure == "different_input":
|
||||
data[0]["runs"][0]["input"]["input_sha256"] = "other"
|
||||
elif failure == "different_ort":
|
||||
data[0]["runs"][0]["ort_version"] = "1.30.0"
|
||||
elif failure == "failed":
|
||||
data[0]["errors"] = ["failed"]
|
||||
else:
|
||||
data[0]["runs"][0]["gpu_elapsed"] = 1.5
|
||||
with pytest.raises(ValueError):
|
||||
derive_threshold(data)
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Device policy, one-time persistence, and calibration failures without GPU hardware."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mineru.config import Config
|
||||
from mineru.model.runtime import onnx_device as policy
|
||||
from mineru.model.runtime.windows_gpu import GpuAdapter, preferred_gpu
|
||||
|
||||
settings = importlib.import_module("mineru.config")
|
||||
|
||||
|
||||
def adapter(*, index: int = 0, uma: bool | None = True, vendor: int = 0x8086, rank: int = 0) -> GpuAdapter:
|
||||
return GpuAdapter(index, f"luid-{index}", "test GPU", vendor, 1, uma, rank)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
target = tmp_path / "config.yaml"
|
||||
target.write_text("model:\n onnx_device: auto\n source: local\n", encoding="utf-8")
|
||||
monkeypatch.setattr(settings, "get_config_file_path", lambda: str(target))
|
||||
monkeypatch.setattr(settings, "get_config_source", lambda key: "file")
|
||||
monkeypatch.setattr(settings.config.model, "onnx_device", "auto")
|
||||
monkeypatch.setattr(policy, "sys", SimpleNamespace(platform="win32"))
|
||||
monkeypatch.setattr(policy, "_selections", {})
|
||||
monkeypatch.setattr(policy, "ONNX_GPU_TIME_RATIO_THRESHOLD", 0.75)
|
||||
monkeypatch.setattr(policy, "discover_gpus", lambda **_: (adapter(),))
|
||||
import onnxruntime
|
||||
from mineru.model import registry
|
||||
from mineru.model.runtime import onnx_calibration
|
||||
|
||||
monkeypatch.setattr(onnxruntime, "get_available_providers", lambda: ["DmlExecutionProvider", "CPUExecutionProvider"])
|
||||
monkeypatch.setattr(registry, "MINERU_4_MODELS_ONNX", SimpleNamespace(ocr_rec=SimpleNamespace(ensure=lambda: "rec.onnx")))
|
||||
monkeypatch.setattr(onnx_calibration, "run_calibration", Mock(return_value={"cpu_elapsed": 2.0, "gpu_elapsed": 1.0}))
|
||||
return target
|
||||
|
||||
|
||||
def test_config_values_and_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert Config().model.onnx_device == "auto"
|
||||
for value in ("auto", "cpu", "gpu"):
|
||||
assert Config(model={"onnx_device": value}).model.onnx_device == value
|
||||
with pytest.raises(ValidationError):
|
||||
Config(model={"onnx_device": "cuda"})
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text("model:\n onnx_device: cpu\n", encoding="utf-8")
|
||||
monkeypatch.setenv("MINERU_CONFIG", str(path))
|
||||
monkeypatch.setenv("MINERU_MODEL_ONNX_DEVICE", "gpu")
|
||||
loaded = settings._load_effective_config()
|
||||
assert loaded.config.model.onnx_device == "gpu"
|
||||
assert loaded.sources[("model", "onnx_device")] == "env"
|
||||
|
||||
|
||||
def test_discrete_priority_and_rank_mapping() -> None:
|
||||
igpu = adapter(index=0, rank=0)
|
||||
discrete = adapter(index=4, uma=False, rank=1)
|
||||
external = adapter(index=7, uma=False, vendor=0x10DE, rank=0)
|
||||
assert preferred_gpu((igpu, discrete, external)) == external
|
||||
assert preferred_gpu((igpu, discrete)) == discrete # Intel discrete is still discrete.
|
||||
assert preferred_gpu(()) is None
|
||||
assert preferred_gpu((adapter(index=2, uma=None, rank=1), igpu)) == igpu
|
||||
|
||||
|
||||
def test_gpu_mode_uses_performance_order_without_creating_devices() -> None:
|
||||
import ctypes
|
||||
from mineru.model.runtime import windows_gpu
|
||||
|
||||
create = Mock(return_value=1) # D3D12CreateDevice returns S_FALSE for a successful capability check.
|
||||
assert windows_gpu._architecture(ctypes.c_void_p(1), create, classify=False) == (True, None)
|
||||
assert create.call_args.args[-1] is None
|
||||
igpu = adapter(index=0, uma=None, rank=1)
|
||||
discrete = adapter(index=1, uma=None, vendor=0x10DE, rank=0)
|
||||
assert preferred_gpu((igpu, discrete)) == discrete
|
||||
|
||||
|
||||
def test_gpu_mode_requests_fast_probe(config_file: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings.config.model, "onnx_device", "gpu")
|
||||
discover = Mock(return_value=(adapter(uma=None),))
|
||||
monkeypatch.setattr(policy, "discover_gpus", discover)
|
||||
assert policy.resolve_onnx_device().device == "gpu"
|
||||
discover.assert_called_once_with(classify=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("gpu_time", "expected"), [(0.74, True), (0.75, True), (0.751, False)])
|
||||
def test_threshold_boundary(gpu_time: float, expected: bool) -> None:
|
||||
assert policy.choose_gpu(1.0, gpu_time, 0.75) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("values", [(0, 1, 0.75), (1, -1, 0.75), (1, float("nan"), 0.75), (1, 1, 1.0)])
|
||||
def test_invalid_timings(values: tuple[float, float, float]) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
policy.choose_gpu(*values)
|
||||
|
||||
|
||||
def test_cpu_never_discovers_or_benchmarks(config_file: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings.config.model, "onnx_device", "cpu")
|
||||
discover = Mock(side_effect=AssertionError("discovery"))
|
||||
monkeypatch.setattr(policy, "discover_gpus", discover)
|
||||
assert policy.resolve_onnx_device().device == "cpu"
|
||||
discover.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("gpu_time", "expected"), [(1.0, "gpu"), (1.6, "cpu")])
|
||||
def test_calibration_saved_once(config_file: Path, monkeypatch: pytest.MonkeyPatch, gpu_time: float, expected: str) -> None:
|
||||
from mineru.model.runtime import onnx_calibration
|
||||
|
||||
measure = Mock(return_value={"cpu_elapsed": 2.0, "gpu_elapsed": gpu_time})
|
||||
monkeypatch.setattr(onnx_calibration, "run_calibration", measure)
|
||||
assert policy.resolve_onnx_device().device == expected
|
||||
assert policy.resolve_onnx_device().device == expected
|
||||
assert measure.call_count == 1
|
||||
assert yaml.safe_load(config_file.read_text())["model"] == {"onnx_device": expected, "source": "local"}
|
||||
policy._selections.clear() # A new process reads the persisted choice instead of benchmarking again.
|
||||
assert policy.resolve_onnx_device().device == expected
|
||||
assert measure.call_count == 1
|
||||
|
||||
|
||||
def test_discrete_skips_calibration_and_persists_only_after_session(config_file: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from mineru.model.runtime import onnx_calibration
|
||||
|
||||
monkeypatch.setattr(policy, "discover_gpus", lambda **_: (adapter(uma=False),))
|
||||
result = policy.resolve_onnx_device()
|
||||
assert result.device == "gpu"
|
||||
onnx_calibration.run_calibration.assert_not_called()
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == "auto"
|
||||
policy.confirm_gpu_session(result)
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == "gpu"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", [RuntimeError("GPU failed"), TimeoutError("worker timeout")])
|
||||
def test_failures_do_not_save_or_retry(config_file: Path, monkeypatch: pytest.MonkeyPatch, failure: Exception) -> None:
|
||||
from mineru.model.runtime import onnx_calibration
|
||||
|
||||
measure = Mock(side_effect=failure)
|
||||
monkeypatch.setattr(onnx_calibration, "run_calibration", measure)
|
||||
assert policy.resolve_onnx_device().device == "cpu"
|
||||
assert policy.resolve_onnx_device().device == "cpu"
|
||||
assert measure.call_count == 1
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == "auto"
|
||||
|
||||
|
||||
def test_environment_choice_is_not_saved(config_file: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "get_config_source", lambda key: "env")
|
||||
assert policy.resolve_onnx_device().device == "gpu"
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == "auto"
|
||||
|
||||
|
||||
def test_user_edit_wins_compare_and_swap(config_file: Path) -> None:
|
||||
config_file.write_text("model:\n onnx_device: cpu\n source: local\n", encoding="utf-8")
|
||||
assert not settings.update_config_file({"model": {"onnx_device": "gpu"}}, only_if_onnx_auto=True)
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == "cpu"
|
||||
assert policy.resolve_onnx_device().device == "cpu"
|
||||
|
||||
|
||||
def test_unwritable_config_keeps_process_choice(config_file: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings, "update_config_file", Mock(side_effect=PermissionError("read only")))
|
||||
assert policy.resolve_onnx_device().device == "gpu"
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == "auto"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["no_gpu", "no_provider", "discovery_error", "pending", "non_windows"])
|
||||
def test_discovery_and_unavailable_states(config_file: Path, monkeypatch: pytest.MonkeyPatch, mode: str) -> None:
|
||||
import onnxruntime
|
||||
|
||||
if mode == "no_gpu":
|
||||
monkeypatch.setattr(policy, "discover_gpus", lambda **_: ())
|
||||
elif mode == "no_provider":
|
||||
monkeypatch.setattr(onnxruntime, "get_available_providers", lambda: ["CPUExecutionProvider"])
|
||||
elif mode == "discovery_error":
|
||||
monkeypatch.setattr(policy, "discover_gpus", Mock(side_effect=OSError("DXGI error")))
|
||||
elif mode == "pending":
|
||||
monkeypatch.setattr(policy, "ONNX_GPU_TIME_RATIO_THRESHOLD", None)
|
||||
else:
|
||||
monkeypatch.setattr(policy, "sys", SimpleNamespace(platform="linux"))
|
||||
assert policy.resolve_onnx_device().device == "cpu"
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == ("cpu" if mode == "no_gpu" else "auto")
|
||||
|
||||
|
||||
def test_manual_gpu_retained_without_gpu(config_file: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(settings.config.model, "onnx_device", "gpu")
|
||||
config_file.write_text("model:\n onnx_device: gpu\n", encoding="utf-8")
|
||||
monkeypatch.setattr(policy, "discover_gpus", lambda **_: ())
|
||||
assert policy.resolve_onnx_device().device == "cpu"
|
||||
assert yaml.safe_load(config_file.read_text())["model"]["onnx_device"] == "gpu"
|
||||
|
||||
|
||||
def test_waiting_process_rechecks_choice_after_lock(config_file: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from mineru.model.runtime import onnx_calibration
|
||||
|
||||
class OtherProcessSaved:
|
||||
def acquire(self) -> None:
|
||||
config_file.write_text("model:\n onnx_device: cpu\n", encoding="utf-8")
|
||||
|
||||
def release(self) -> None:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(policy, "FileLock", lambda *args, **kwargs: OtherProcessSaved())
|
||||
assert policy.resolve_onnx_device().device == "cpu"
|
||||
onnx_calibration.run_calibration.assert_not_called()
|
||||
|
||||
|
||||
def test_concurrent_model_initializers_share_one_decision(config_file: Path) -> None:
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from mineru.model.runtime import onnx_calibration
|
||||
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
choices = list(pool.map(lambda _: policy.resolve_onnx_device(), range(8)))
|
||||
assert all(choice is choices[0] for choice in choices)
|
||||
assert onnx_calibration.run_calibration.call_count == 1
|
||||
|
||||
|
||||
def test_manual_reset_to_auto_recalibrates(config_file: Path) -> None:
|
||||
from mineru.model.runtime import onnx_calibration
|
||||
|
||||
policy.resolve_onnx_device()
|
||||
settings.update_config_file({"model": {"onnx_device": "auto"}})
|
||||
policy._selections.clear() # New process after the explicit reset.
|
||||
policy.resolve_onnx_device()
|
||||
assert onnx_calibration.run_calibration.call_count == 2
|
||||
@@ -107,7 +107,7 @@ def test_ocr_txt_dictionary_ctc_space_and_shape_validation(monkeypatch: pytest.M
|
||||
get_inputs=lambda: [SimpleNamespace(name="image")],
|
||||
get_outputs=lambda: [SimpleNamespace(shape=["batch", "width", 4])],
|
||||
)
|
||||
monkeypatch.setattr(ocr, "ort_session", lambda *args: session)
|
||||
monkeypatch.setattr(ocr, "ort_session", lambda *args, **kwargs: session)
|
||||
recognizer = ocr.TextRecognizerONNX("rec.onnx", str(dictionary))
|
||||
logits = np.eye(4, dtype=np.float32)[[1, 1, 0, 2, 3, 3, 0, 1]]
|
||||
assert recognizer._decode(logits) == ("中A 中", 1.0)
|
||||
@@ -116,7 +116,7 @@ def test_ocr_txt_dictionary_ctc_space_and_shape_validation(monkeypatch: pytest.M
|
||||
dictionary.write_text("中\nA\nB\n", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="dictionary requires"):
|
||||
ocr.TextRecognizerONNX("rec.onnx", str(dictionary))
|
||||
assert len(PPOCRV6_DICT_PATH.read_text().splitlines()) == 18708
|
||||
assert len(PPOCRV6_DICT_PATH.read_text(encoding="utf-8").splitlines()) == 18708
|
||||
|
||||
|
||||
def test_formula_invalid_boxes_do_not_shift_latex_between_items(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -124,6 +124,7 @@ def test_formula_invalid_boxes_do_not_shift_latex_between_items(monkeypatch: pyt
|
||||
from mineru.model.mfr.pp_formulanet_plus_m_onnx import PPFormulaNetPlusMONNX
|
||||
|
||||
model = object.__new__(PPFormulaNetPlusMONNX)
|
||||
model.cpu_tail_enabled = False
|
||||
monkeypatch.setattr(model, "_infer_batch", lambda crops, batch_size: ["first", "second"])
|
||||
source = [
|
||||
[{"label": "display_formula", "bbox": [-8, 0, -1, 5]}, {"label": "inline_formula", "bbox": [1, 1, 8, 8]}],
|
||||
@@ -165,6 +166,7 @@ def test_formula_eos_stops_before_trailing_tokens() -> None:
|
||||
from mineru.model.mfr.pp_formulanet_plus_m_onnx import PPFormulaNetPlusMONNX
|
||||
|
||||
model = object.__new__(PPFormulaNetPlusMONNX)
|
||||
model.cpu_tail_enabled = False
|
||||
model.decoder = Mock(return_value=["x"])
|
||||
assert model._decode_tokens(np.array([[0, 17, 2, 99, 1]])) == "x"
|
||||
np.testing.assert_array_equal(model.decoder.call_args.args[0], [[0, 17, 2]])
|
||||
@@ -333,6 +335,7 @@ def test_formula_cpu_batch_eight_sorts_and_restores_order(monkeypatch: pytest.Mo
|
||||
from mineru.model.mfr.pp_formulanet_plus_m_onnx import PPFormulaNetPlusMONNX
|
||||
|
||||
model = object.__new__(PPFormulaNetPlusMONNX)
|
||||
model.cpu_tail_enabled = False
|
||||
model.input_name = "image"
|
||||
calls = []
|
||||
|
||||
|
||||
@@ -122,6 +122,17 @@ def test_explicit_cpu_overrides_available_cuda_provider(monkeypatch: pytest.Monk
|
||||
assert factory.call_args.kwargs["providers"] == onnx.ort_providers()
|
||||
|
||||
|
||||
def test_explicit_onnx_stack_keeps_tables_cpu_even_with_cuda(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Per-model backend overrides must win over a globally selected Torch backend."""
|
||||
monkeypatch.setenv("MINERU_TABLE_DEVICE", "cuda")
|
||||
monkeypatch.setattr(onnx.ort, "get_available_providers", lambda: ["CUDAExecutionProvider", "CPUExecutionProvider"])
|
||||
session = SimpleNamespace(get_providers=lambda: ["CPUExecutionProvider"])
|
||||
factory = Mock(return_value=session)
|
||||
monkeypatch.setattr(onnx.ort, "InferenceSession", factory)
|
||||
assert onnx.table_ort_session("table.onnx", small_backend="onnx") is session
|
||||
assert factory.call_args.kwargs["providers"] == onnx.ort_providers()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device", ["auto", "cuda"])
|
||||
@pytest.mark.parametrize("mode", ["unavailable", "exception", "silent"])
|
||||
def test_cuda_failure_is_reported_and_cpu_remains_available(monkeypatch: pytest.MonkeyPatch, mode: str, device: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user