docling-project / docling-project/docling
ThreadedDoclingParseDocumentBackend drops tables that DoclingParseDocumentBackend detects (same pipeline options, reproduced at parser_threads=1)
- Dominant language
- Python
- Stars
- 66.4k
- Forks
- 4.8k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 84
Description
### Summary
Switching **only** the PDF backend from `DoclingParseDocumentBackend` to `ThreadedDoclingParseDocumentBackend` — with identical `ThreadedPdfPipelineOptions` and the same `StandardPdfPipeline` — causes a table to be missed. The serial backend extracts it as a `TableItem`; the threaded backend emits its content as loose text and produces **zero** tables.
The divergence is in the backend, not the table model (TableFormer is identical across both runs) and not concurrency (it persists at `parser_threads=1`).
### Environment
- `docling` 2.96.0
- `docling-core` 2.77.1
- `docling-parse` 6.2.0
- `docling-ibm-models` 3.13.2
- Python 3.14, macOS (Apple Silicon, arm64)
### Reproduction (fully self-contained — generates its own PDF)
```python
import warnings; warnings.filterwarnings("ignore")
# 1. Generate a minimal A4 PDF containing one simple table (matplotlib only).
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8.27, 11.69))
ax.axis("off")
ax.text(0.5, 0.95, "Contacts available for customer meetings", ha="center", fontsize=14)
rows = [["Area of expertise", "Product Management", "Product Marketing"],
["Document Cloud", "Vamsi Vutukuru", "Nora Yau"],
["Acrobat", "Alex Chen", "Maria Lopez"],
["Sign", "Sam Patel", "Lena Frei"]]
tbl = ax.table(cellText=rows[1:], colLabels=rows[0], loc="center", cellLoc="center")
tbl.auto_set_font_size(False); tbl.set_fontsize(11); tbl.scale(1, 2.2)
fig.savefig("/tmp/table_repro.pdf"); plt.close(fig)
# 2. Convert with each backend — identical pipeline options, vary only the backend.
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.base_models import InputFormat
from docling.backend.docling_parse_backend import (
DoclingParseDocumentBackend, ThreadedDoclingParseDocumentBackend,
)
from docling.datamodel.backend_options import (
ThreadedDoclingParseBackendOptions, PdfBackendOptions,
)
from docling.datamodel.pipeline_options import ThreadedPdfPipelineOptions
def run(label, backend, opts):
fo = PdfFormatOption(pipeline_options=ThreadedPdfPipelineOptions(),
backend=backend, backend_options=opts)
res = DocumentConverter(format_options={InputFormat.PDF: fo}).convert("/tmp/table_repro.pdf")
print(f"{label}: tables={len(res.document.tables)} texts={len(res.document.texts)}")
run("serial ", DoclingParseDocumentBackend, PdfBackendOptions())
run("threaded", ThreadedDoclingParseDocumentBackend, ThreadedDoclingParseBackendOptions())
run("threaded(parser_threads=1)", ThreadedDoclingParseDocumentBackend,
ThreadedDoclingParseBackendOptions(parser_threads=1))
```
### Observed
```
serial : tables=1 texts=1
threaded: tables=0 texts=2
threaded(parser_threads=1): tables=0
```
The serial backend detects the table; the threaded backend produces none, and the table's cell text is demoted into loose text items.
### Expected
Equal table extraction regardless of the PDF backend — `ThreadedDoclingParseDocumentBackend` at table-extraction parity with `DoclingParseDocumentBackend`.
### Notes
- **Not the table model.** TableFormer is identical in all three runs; only the backend differs.
- **Not concurrency.** The loss persists at `parser_threads=1` (fully serialised), so it is not a race / ordering bug.
- The threaded backend appears to feed different cell/layout data into the downstream table-structure stage, which then fails to cluster the table.
- Reproduced first on a real-world deck and then minimised to the self-contained PDF above.
Contributor guide
Assessment
This issue has not been assessed yet.