Skip to content

refactor(layout): run all layout inference through the object-detection factory path - #3914

Merged
cau-git merged 11 commits into
mainfrom
cau/remove-drift-legacy-layout
Jul 31, 2026
Merged

refactor(layout): run all layout inference through the object-detection factory path#3914
cau-git merged 11 commits into
mainfrom
cau/remove-drift-legacy-layout

Conversation

@cau-git

@cau-git cau-git commented Jul 30, 2026

Copy link
Copy Markdown
Member

Why

Layout detection had two implementations that had drifted apart:

  • LayoutModel, backed by docling_ibm_models.layoutmodel.LayoutPredictor — the default, but locked to a single inference runtime.
  • LayoutObjectDetectionModel, backed by AutoModelForObjectDetection — supports the pluggable engine system (transformers / ONNX Runtime / KServe v2) and presets, but was not the default and had accumulated its own defects.

Both run the same weights (docling-layout-heron) at the same 0.3 score threshold, so this is a defaults-and-plumbing change, not an accuracy change. But they disagreed on postprocessing defaults, bounding-box handling and debug hooks, which meant every layout change had to be made twice and the two paths could produce different documents from the same PDF.

This collapses them onto one. LayoutObjectDetectionModel becomes the only code that runs layout inference, and nothing under docling/ imports docling_ibm_models.layoutmodel any more. The dependency itself stays — it is still required by the reading-order and table-structure models.

What changed

Object-detection model fixes

  • Batched inference. predict_layout looped page-by-page calling engine.predict(), which wraps each image in predict_batch([x]). The GPU batch was pinned to 1 regardless of page_batch_size / layout_batch_size. Now one predict_batch call per page batch. This also fixes TimeRecorder("layout"), which was opened once per page instead of once per batch.
  • Bounding boxes are clamped to the page after page-space scaling. LayoutPostprocessor compares cluster.bbox.area() / page_area against a 0.90 full-page threshold and uses intersection_over_self for containment; an overshooting box inflates only the numerator, so a picture covering ~88% of a page could cross the threshold and stop adopting its text children.
  • Unmapped label ids are dropped and warned about once, instead of being silently relabelled as TEXT.
  • XPU support restored in the transformers engine (_resolve_device only offered CPU/CUDA/MPS).
  • settings.debug.visualize_raw_layout now fires on this path; it previously existed only on the legacy model.

Defaults and plumbing

  • Presets registered for layout_heron_101, layout_egret_medium, layout_egret_large, layout_egret_xlarge. Only layout_heron_default existed, so the higher-accuracy models the docs recommend were not actually selectable.
  • engine_options has a default, so LayoutObjectDetectionOptions() constructs without arguments.
  • create_orphan_clusters moved up to BaseLayoutOptions with one True default. The two option classes declaring it with different defaults was the drift that made the paths produce different documents, and it forced this in three pipelines:
    create_orphan = (
        lo.create_orphan_clusters
        if isinstance(lo, (LayoutOptions, LayoutObjectDetectionOptions))
        else False
    )
    Behaviour is unchanged for TableCropsLayoutOptions, the third implementer: it sets requires_layout_postprocessing = False, so the postprocessor never reads the flag.
  • docling-tools models download now fetches every engine variant of the layout model, including docling-layout-heron-onnx. Air-gapped deployments selecting the ONNX engine previously failed at conversion time because that repository was never prefetched.

Deprecations — nothing stops working

  • LayoutOptions and LayoutModel emit DeprecationWarning. LayoutModel is now a ~50-line shim that warns, translates the options and delegates; it inherits predict_layout. LayoutOptions still constructs, still selects any of the five supported models, and still resolves through LayoutFactory.
  • DOCLING_LAYOUT_V2 (docling-layout-old) is no longer supported. The symbol stays, but selecting it warns and falls back to Heron. It is the only model in the catalog with a class that is not a DocItemLabel (background at index 0), and retiring it means no label-map special-casing anywhere.
  • The artifacts_path/<model_path> direct-pointing fallback is gone. It already emitted a DeprecationWarning, and the standard artifacts_path/<repo--folder> convention is unaffected.

layout_model_specs.py, LayoutModelConfig and the DOCLING_LAYOUT_* constants now exist solely to serve LayoutOptions. They carry comments recording that they can be deleted in the same release that removes it.

Behaviour changes to be aware of

  • torch.compile is now on for the layout model by default (settings.inference.compile_torch_models). LayoutPredictor never compiled. Steady-state inference is faster, but there is a per-process cold-start cost of roughly 15s that one-shot CLI and serverless users will notice. Opt out with settings.inference.compile_torch_models = False.
  • Sub-pixel bounding-box shift on pages whose size is not an integer number of points. The engine returns boxes in image-pixel space; the legacy path never scaled them back to page space, assuming the scale-1.0 raster was exactly page-sized. Worst case is ~0.5px at the far edge. US Letter pages have a scale factor of exactly 1.0 and are unaffected.

Verification

  • Full test suite green except the two items below.
  • End-to-end parity: the existing test_e2e_conversion corpus already is a cross-revision parity gate, since it converts with whatever the default is and compares against ground truth generated under the old default. Across the whole ml_pdf_model suite, the only assertion that moved was the sub-pixel scaling case above, confirmed arithmetically rather than by inspection (388.67487 × 595.32/595 = 388.88387). torch.compile on/off gives byte-identical boxes, so the shift is not numerical noise.
  • New tests/test_layout_migration.py covers: the shim contract for all five model specs; option round-tripping; the DOCLING_LAYOUT_V2 fallback; factory dispatch for both option types; label maps for every supported repository (config-only, no weights, no inference); single-batched-call and page-skipping behaviour; picture-internal text survival; and a grep-level assertion that docling_ibm_models.layoutmodel is not imported under docling/.
  • Air-gapped conversion verified offline for the transformers engine, the ONNX engine and the deprecated LayoutOptions path — all three succeed and produce identical output.

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

`LayoutObjectDetectionModel` becomes the only code that runs layout
inference and `LayoutObjectDetectionOptions` becomes the default layout
options. `docling_ibm_models.layoutmodel.LayoutPredictor` is no longer
imported anywhere under `docling/`.

Two layout implementations had drifted apart: the legacy `LayoutModel`
via `docling-ibm-models`, and `LayoutObjectDetectionModel` via
`AutoModelForObjectDetection`. Both ran the same weights at the same
score threshold, but with different postprocessing defaults, different
bounding-box handling and different debug hooks - and only the legacy one
was reachable by default, while only the new one supported pluggable
inference engines. Keeping both meant every layout change had to be made
twice.

Object-detection model fixes:

- Run inference as a single `predict_batch` call instead of one call per
  page, so `page_batch_size` / `layout_batch_size` reach the GPU again,
  and `TimeRecorder("layout")` covers the batch rather than each page.
- Clamp bounding boxes to the page after page-space scaling.
  `LayoutPostprocessor` compares cluster area against page area, so an
  overshooting box can cross the 0.90 full-page threshold and stop
  adopting its children.
- Drop detections whose label id is absent from the model's own
  `id2label` instead of silently relabelling them as TEXT.
- Restore XPU support in the transformers engine.
- Emit `settings.debug.visualize_raw_layout` output, which previously
  existed only on the legacy path.

Defaults and plumbing:

- Register presets for heron-101 and the three egret variants, so the
  documented higher-accuracy recommendations are actually selectable.
- Give `engine_options` a default so `LayoutObjectDetectionOptions()`
  constructs without arguments.
- Promote `create_orphan_clusters` to `BaseLayoutOptions` with a single
  `True` default. The two option classes disagreeing on it was the drift
  that made the paths produce different documents, and it forced
  `isinstance` branching in three pipelines.
- Prefetch every engine variant of the layout model, including the ONNX
  repository, which air-gapped ONNX deployments never received.

`LayoutOptions` and `LayoutModel` are deprecated but keep working:
`LayoutModel` is now a shim that warns, translates the options and
delegates to `LayoutObjectDetectionModel`. `DOCLING_LAYOUT_V2` is no
longer supported and falls back to Heron with a warning.

The only intended output change is a sub-pixel bounding-box shift on
pages whose size is not an integer number of points: the engine returns
boxes in image-pixel space, and the legacy path never scaled them back
to page space.

Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
@github-actions

Copy link
Copy Markdown
Contributor

DCO Check Passed

Thanks @cau-git, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 2 merge protections satisfied — ready to merge.

Show 2 satisfied protections

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

🟢 Require two reviewer for test updates

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

@cau-git
cau-git marked this pull request as draft July 30, 2026 12:18
@cau-git cau-git changed the title feat(layout): run all layout inference through the object-detection path refactor(layout): run all layout inference through the object-detection path Jul 30, 2026
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
@cau-git cau-git changed the title refactor(layout): run all layout inference through the object-detection path refactor(layout): run all layout inference through the object-detection factory path Jul 30, 2026
cau-git added 3 commits July 30, 2026 16:20
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
@cau-git
cau-git marked this pull request as ready for review July 31, 2026 08:33
cau-git added 3 commits July 31, 2026 11:30
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
@cau-git

cau-git commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Note: Generated outputs of this PR was also checked against 500 doclaynet test set samples, with no regression in any metrics.

@cau-git
cau-git requested a review from PeterStaar-IBM July 31, 2026 11:53
dolfim-ibm
dolfim-ibm previously approved these changes Jul 31, 2026

@dolfim-ibm dolfim-ibm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
PeterStaar-IBM
PeterStaar-IBM previously approved these changes Jul 31, 2026

@PeterStaar-IBM PeterStaar-IBM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

Comment thread docling/datamodel/pipeline_options.py Outdated
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>

@PeterStaar-IBM PeterStaar-IBM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@cau-git
cau-git merged commit bbdc862 into main Jul 31, 2026
46 checks passed
@cau-git
cau-git deleted the cau/remove-drift-legacy-layout branch July 31, 2026 13:58
Anai-Guo added a commit to Anai-Guo/docling that referenced this pull request Aug 9, 2026
torch.compile() is lazy: the backend only builds a kernel on the first
forward pass, so on a machine without a working C++ compiler the failure
surfaces in predict_batch() rather than in initialize(). Nothing catches
it there, so it propagates as a ConversionError and the whole conversion
fails.

Since docling-project#3914 made LayoutObjectDetectionOptions the default and
compile_torch_models defaults to True, this is on the default PDF path,
so a plain convert() raises on any environment without a compiler --
Windows without MSVC, but also slim Linux images without g++.

Compilation is an optimization, so keep a handle on the uncompiled model
and degrade to it with a warning instead of failing the conversion.
The swap is permanent, so later batches do not retry compilation. A
missing handle, and errors that are not TorchDynamoException, still
propagate unchanged.

Signed-off-by: Tai An <antai12232931@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants