Repository navigation
Description
Description of the bug
Upgrading from PyMuPDF 1.28.2 to 2.0 adds layout-model initialization and background threads to a plain import pymupdf, even when the application does not use layout features. No input document is needed to reproduce this.
This is costly for applications that use fresh spawned subprocesses. Each child repeats the import and model initialization. In our service, children run in a container with a 2-core CPU quota on a larger host. The extra import work caused severe CPU throttling under load.
In production, the ONNX pool had 64 threads on a host with 64 logical CPUs, although the container had a CPU quota of only 2 cores. The pool therefore appeared to size itself from host CPU availability rather than the container’s CPU quota. Each fresh subprocess repeated this initialization. This is a production observation, separate from the smaller local reproduction below.
The 2.0 package imports pymupdf.layout during package initialization, and the layout module calls activate() at import time. Could layout-model initialization be deferred until a layout operation is requested, or could the full package offer a documented opt-out?
We have seen the new PyMuPDF Lite documentation and the merged documentation update. Lite may be the intended migration path for our workload. If so, please clarify that the full 2.0 package activates layout during a plain import, and that applications using short-lived subprocesses should migrate to Lite.
How to reproduce the bug
Save this Linux-only script as probe.py. The --block-layout mode is a diagnostic control, not a proposed supported workaround.
import importlib.metadata
import json
import os
from pathlib import Path
import platform
import sys
import time
# Diagnostic control: simulate an unavailable optional layout module.
if '--block-layout' in sys.argv:
sys.modules['pymupdf.layout'] = None
def threads():
return len(list(Path('/proc/self/task').iterdir()))
before = threads()
wall = time.perf_counter()
cpu = time.process_time()
import pymupdf
cpu_elapsed = time.process_time() - cpu
wall_elapsed = time.perf_counter() - wall
print(json.dumps({
'python': platform.python_version(),
'arch': platform.machine(),
'pymupdf': importlib.metadata.version('pymupdf'),
'onnxruntime': importlib.metadata.version('onnxruntime') if importlib.util.find_spec('onnxruntime') else None,
'visible_cpus': os.cpu_count(),
'cpu_max': Path('/sys/fs/cgroup/cpu.max').read_text().strip(),
'block_layout': '--block-layout' in sys.argv,
'import_cpu_seconds': round(cpu_elapsed, 3),
'import_wall_seconds': round(wall_elapsed, 3),
'threads_before': before,
'threads_after': threads(),
'layout_loaded': sys.modules.get('pymupdf.layout') is not None,
'onnx_loaded': 'onnxruntime' in sys.modules,
}))Run it in a disposable container:
docker run --rm --cpus=2 -v "$PWD/probe.py:/repro/probe.py:ro" \
python:3.14-slim-bookworm sh -c '
pip install pymupdf==1.28.2 &&
python /repro/probe.py &&
pip install pymupdf==2.0 onnxruntime==1.31.0 &&
python /repro/probe.py &&
python /repro/probe.py --block-layout
'Each invocation runs in a fresh interpreter. The script imports the package but does not open a document or call a layout API.
Observed in a Linux aarch64 container, Python 3.14.7, four visible CPUs, and cpu.max = 200000 100000 (2-core quota):
| Configuration | Import CPU seconds | Import wall seconds | Threads before → after | Layout loaded |
|---|---|---|---|---|
| PyMuPDF 1.28.2 | 0.301 | 0.352 | 1 → 1 | No |
| PyMuPDF 2.0, ONNX Runtime 1.31.0 | 0.692 | 0.610 | 1 → 14 | Yes |
| Same 2.0 install, layout import blocked | 0.193 | 0.133 | 1 → 5 | No |
These are single-run observations, not a statistical benchmark. Absolute timings and thread counts vary with the machine. The blocked-layout control still had onnxruntime in sys.modules; it isolates layout activation, not every ONNX-related import.
For additional context, an application-module import in our production container used 8.6 CPU-seconds and left 158 threads with layout enabled. Blocking layout reduced that to 2.7 CPU-seconds and 65 threads. Those figures include other application dependencies and are not measurements of import pymupdf alone. Our subprocess pool uses spawn, max_workers=2, and max_tasks_per_child=1, so this cost recurs for each task.
Expected behavior: core PDF operations should not require layout-model initialization before a layout feature is used. We are reporting import overhead and its effect under CPU quotas; we are not claiming that PyMuPDF itself deadlocks.
PyMuPDF version
2.0 (comparison: 1.28.2)
Operating system
Linux
Python version
3.14 (reproduction: 3.14.7)
Activity
JorjMcKie commentedon Oct 22, 2026
We are currently developing a fix that postpones the imports of layout and pymupdf4llm until their first actual use.
Import duration comparisons indicate that this will bring us back to original response times.
build: bump fastapi to 0.161.1 and skip PyMuPDF 2.0 (#648)
julian-smith-artifex-com commentedon Nov 2, 2026
Fixed in PyMuPDF-2.1.