From ca53a3ac4b5f85b6cd244ef97832854e1e8739e2 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 12 Aug 2026 09:05:00 +0100 Subject: [PATCH] chore: bump docling --- application/core/settings.py | 1 + application/parser/document_reader.py | 3 + application/parser/file/docling_parser.py | 25 +++++ application/requirements.txt | 2 +- docs/content/Guides/ocr.mdx | 16 +++ tests/parser/file/test_docling_parser.py | 120 ++++++++++++++++++++++ tests/parser/test_document_reader.py | 48 +++++++++ 7 files changed, 214 insertions(+), 1 deletion(-) diff --git a/application/core/settings.py b/application/core/settings.py index e568cf44..12d8924f 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -95,6 +95,7 @@ class Settings(BaseSettings): # Pages docling's threaded pipeline buffers in flight; the library # default (100) drives worker RSS to ~3 GB on a mid-size PDF. DOCLING_PIPELINE_QUEUE_MAX_SIZE: int = 2 + DOCLING_COMPILE_TORCH_MODELS: bool = False DOCLING_TABULAR_MAX_BYTES: int = 2_000_000 DOCLING_MARKUP_MAX_BYTES: int = 8_000_000 ATTACHMENT_TEXT_MAX_BYTES: int = 5_000_000 diff --git a/application/parser/document_reader.py b/application/parser/document_reader.py index 752f7c33..978e29bd 100644 --- a/application/parser/document_reader.py +++ b/application/parser/document_reader.py @@ -252,6 +252,9 @@ def _docling_structured(path: Path, *, ocr_enabled: bool, include_tables: bool, else: from docling.document_converter import DocumentConverter + from application.parser.file.docling_parser import _apply_inference_settings + + _apply_inference_settings() converter = DocumentConverter() doc = converter.convert(str(path)).document markdown = doc.export_to_markdown() diff --git a/application/parser/file/docling_parser.py b/application/parser/file/docling_parser.py index 74c2018e..9272ff8c 100644 --- a/application/parser/file/docling_parser.py +++ b/application/parser/file/docling_parser.py @@ -43,6 +43,29 @@ def _apply_pipeline_caps(pipeline_options) -> None: setattr(pipeline_options, name, value) +def _apply_inference_settings() -> None: + """Set docling's global torch.compile toggle from ``DOCLING_COMPILE_TORCH_MODELS``. + + docling compiles its layout/table/OCR models with ``torch.compile`` by + default. For DocsGPT's one-shot, per-file parses that warmup is never + amortized, and it hard-fails wherever TorchInductor cannot build: it emits + invalid Metal on Apple Silicon, and its C++ path does not quote the ``-L`` + library path, so any install directory containing a space fails to build. + + Guarded so docling builds without the inference settings are unaffected. + """ + from application.core.settings import settings + + try: + from docling.datamodel.settings import settings as docling_settings + except ImportError: # pragma: no cover - docling always present here + return + + inference = getattr(docling_settings, "inference", None) + if inference is not None and hasattr(inference, "compile_torch_models"): + inference.compile_torch_models = settings.DOCLING_COMPILE_TORCH_MODELS + + def _tabular_content_size(file: Path) -> int: """Effective content size of a tabular file, in bytes. @@ -256,6 +279,8 @@ class DoclingParser(BaseParser): ) from docling.datamodel.pipeline_options import PdfPipelineOptions + _apply_inference_settings() + pipeline_options = PdfPipelineOptions( do_ocr=self.ocr_enabled, do_table_structure=self.table_structure, diff --git a/application/requirements.txt b/application/requirements.txt index 2a0c740d..061648d0 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -12,7 +12,7 @@ cryptography==50.0.0 dataclasses-json==0.6.7 daytona==0.203.0 defusedxml==0.7.1 -docling==2.84.0 +docling==2.119.0 rapidocr==3.9.2 onnxruntime==1.28.0 docx2txt==0.9 diff --git a/docs/content/Guides/ocr.mdx b/docs/content/Guides/ocr.mdx index 3626e13c..9ac09af3 100644 --- a/docs/content/Guides/ocr.mdx +++ b/docs/content/Guides/ocr.mdx @@ -51,6 +51,22 @@ By default, Docling parser classes use RapidOCR options (language default: `engl Parser internals like OCR language and force-full-page OCR are currently set by code defaults, not separate `.env` settings. +### Model compilation + +Docling runs its layout, table, and OCR models through `torch.compile` by +default. DocsGPT turns that off. On x86-64 Linux, compiling raised the first +parse of a two-page PDF from 8.4s to 58.6s while steady-state parsing stayed +at 1.5s either way, so the warmup is overhead a per-file parse never recovers. +Compiling also fails outright on Apple Silicon, on install paths containing a +space, on Windows without MSVC, and in slim images with no C compiler. + +```env +DOCLING_COMPILE_TORCH_MODELS=false +``` + +Set it to `true` only if you are parsing large batches on a machine where the +warmup pays for itself. + ## Attachment Behavior by Model Support When attachments are used in chat, behavior depends on the selected model/provider: diff --git a/tests/parser/file/test_docling_parser.py b/tests/parser/file/test_docling_parser.py index 83612e3a..83caaeef 100644 --- a/tests/parser/file/test_docling_parser.py +++ b/tests/parser/file/test_docling_parser.py @@ -891,3 +891,123 @@ class TestTabularGateSeam: with pytest.raises(DocumentParseError): DoclingCSVParser().parse_file(path) + + +# ===================================================================== +# torch.compile inference setting +# ===================================================================== + + +@pytest.mark.unit +class TestApplyInferenceSettings: + """_apply_inference_settings governs docling's torch.compile of its models. + + docling >= 2.92 compiles its layout/OCR models with torch.compile by + default, which hard-fails TorchInductor's Metal codegen on Apple Silicon + and buys nothing for one-shot parses. + """ + + def test_disables_torch_compile_by_default(self, monkeypatch): + from application.parser.file.docling_parser import _apply_inference_settings + + class Inference: + compile_torch_models = True + + class DoclingSettings: + inference = Inference() + + docling_settings = DoclingSettings() + monkeypatch.setattr( + "docling.datamodel.settings.settings", docling_settings, raising=False + ) + + _apply_inference_settings() + + assert docling_settings.inference.compile_torch_models is False + + def test_opt_in_reenables_torch_compile(self, monkeypatch): + from application.core.settings import settings + from application.parser.file.docling_parser import _apply_inference_settings + + monkeypatch.setattr( + settings, "DOCLING_COMPILE_TORCH_MODELS", True, raising=False + ) + + class Inference: + compile_torch_models = False + + class DoclingSettings: + inference = Inference() + + docling_settings = DoclingSettings() + monkeypatch.setattr( + "docling.datamodel.settings.settings", docling_settings, raising=False + ) + + _apply_inference_settings() + + assert docling_settings.inference.compile_torch_models is True + + def test_noop_on_docling_without_inference_settings(self, monkeypatch): + """Builds predating the inference settings must be a silent no-op.""" + from application.parser.file.docling_parser import _apply_inference_settings + + class DoclingSettings: + pass + + monkeypatch.setattr( + "docling.datamodel.settings.settings", DoclingSettings(), raising=False + ) + + _apply_inference_settings() # must not raise + + def test_create_converter_applies_inference_settings(self, monkeypatch): + """The cap is worthless unless the converter path actually calls it.""" + from application.parser.file.docling_parser import DoclingParser + + called = [] + monkeypatch.setattr( + "application.parser.file.docling_parser._apply_inference_settings", + lambda: called.append(True), + ) + monkeypatch.setattr( + "docling.document_converter.DocumentConverter", + MagicMock(), + ) + + DoclingParser(ocr_enabled=False)._create_converter() + + assert called == [True] + + def test_inference_settings_applied_before_options_are_built(self, monkeypatch): + """Ordering is load-bearing, not incidental. + + ``compile_model`` is a ``default_factory`` that reads docling's global + at *option construction* time, so flipping the global after building + ``PdfPipelineOptions`` is silently ignored. Pin the order. + """ + import docling.datamodel.pipeline_options as dpo + + from application.parser.file.docling_parser import DoclingParser + + events = [] + monkeypatch.setattr( + "application.parser.file.docling_parser._apply_inference_settings", + lambda: events.append("settings"), + ) + + real_options = dpo.PdfPipelineOptions + + def _tracking_options(*args, **kwargs): + events.append("options") + return real_options(*args, **kwargs) + + monkeypatch.setattr(dpo, "PdfPipelineOptions", _tracking_options) + monkeypatch.setattr("docling.document_converter.DocumentConverter", MagicMock()) + + DoclingParser(ocr_enabled=False)._create_converter() + + assert events == ["settings", "options"], ( + "torch.compile must be disabled before PdfPipelineOptions is " + f"constructed, got {events}" + ) diff --git a/tests/parser/test_document_reader.py b/tests/parser/test_document_reader.py index 7d594eb0..83d2e435 100644 --- a/tests/parser/test_document_reader.py +++ b/tests/parser/test_document_reader.py @@ -516,3 +516,51 @@ def test_bound_parse_payload_small_content_not_flagged(): out = bound_parse_payload({"output": "text", "content": "hi"}, max_chars=10) assert out["content"] == "hi" assert out["truncated"] is False + + +# --------------------------------------------------------------------------- +# vanilla-converter fallback honors the torch.compile toggle +# --------------------------------------------------------------------------- +@pytest.mark.unit +def test_vanilla_converter_applies_inference_settings(monkeypatch, tmp_path): + """The fallback builds its own DocumentConverter, so it must disable + torch.compile itself — the configured-parser path can't do it for it.""" + import sys + import types + + called = [] + monkeypatch.setattr( + "application.parser.file.docling_parser._apply_inference_settings", + lambda: called.append(True), + ) + + class _Doc: + texts = [] + tables = [] + pages = {} + + def export_to_markdown(self): + return "# md" + + def export_to_dict(self): + return {"texts": []} + + class _Converter: + def convert(self, *a, **k): + assert called == [True], "converter built before torch.compile was disabled" + return types.SimpleNamespace(document=_Doc()) + + fake_docling = types.ModuleType("docling") + fake_dc_module = types.ModuleType("docling.document_converter") + fake_dc_module.DocumentConverter = _Converter + fake_docling.document_converter = fake_dc_module + monkeypatch.setitem(sys.modules, "docling", fake_docling) + monkeypatch.setitem(sys.modules, "docling.document_converter", fake_dc_module) + + path = tmp_path / "doc.pdf" + path.write_bytes(b"%PDF") + + out = dr._docling_structured(path, ocr_enabled=False, include_tables=False, parser=None) + + assert out["markdown"] == "# md" + assert called == [True]