Skip to content

PP-DocLayoutV3: pdparams2safetensors conversion yields empty detections under transformers/HF (Linear weight layout) #5202

Open

Description

@abhijeet-kaith

Summary

Fine-tuned PP-DocLayoutV3 weights converted via Global.mode=pdparams2safetensors load successfully in Hugging Face / PaddleX engine="transformers", but produce no detections (all scores ≈ 0.05). The same checkpoint works correctly with paddle static inference (best_model/inference). The official hub model PaddlePaddle/PP-DocLayoutV3_safetensors also works under HF.

This points to a pdparams → safetensors Linear weight layout bug (reshape vs transpose / incomplete transpose), not a training or post-process issue.

Environment

  • Model: PP-DocLayoutV3 (fine-tuned, n classes)
  • Conversion: python main.py -c paddlex/configs/modules/layout_analysis/PP-DocLayoutV3.yaml -o Global.mode=pdparams2safetensors ...
  • Inference engines compared:
    • ✅ PaddleX paddle static (model_dir=.../best_model/inference)
    • ✅ HF hub PaddlePaddle/PP-DocLayoutV3_safetensors
    • ❌ Converted local model.safetensors + engine="transformers" / AutoModelForObjectDetection.from_pretrained

Repro

1) Convert

python main.py -c paddlex/configs/modules/layout_analysis/PP-DocLayoutV3.yaml \
  -o Global.mode=pdparams2safetensors \
  -o Pdparams2safetensors.input_path=./output/best_model/best_model.pdparams \
  -o Pdparams2safetensors.output_dir=./output/safetensors/

(Also set config.json / inference.yml label_list / id2label / num_labels to match the fine-tuned class count — exporter currently writes the default 25-class list.)

2) Infer with transformers engine

from paddlex import create_model

model = create_model(
    model_name="PP-DocLayoutV3",
    model_dir="./output/safetensors",
    engine="transformers",
)
for res in model.predict("page.png", threshold=0.25):
    res.print()  # boxes: []

Or via Hugging Face:

from transformers import AutoImageProcessor, AutoModelForObjectDetection
from PIL import Image

model = AutoModelForObjectDetection.from_pretrained("./output/safetensors")
processor = AutoImageProcessor.from_pretrained("./output/safetensors")
image = Image.open("page.png").convert("RGB")
inputs = processor(images=image, return_tensors="pt")
outputs = model(**inputs)
results = processor.post_process_object_detection(
    outputs, threshold=0.25, target_sizes=[image.size[::-1]]
)
print(len(results[0]["scores"]))  # 0

3) Control: paddle static on same page

model = create_model(
    model_name="PP-DocLayoutV3",
    model_dir="./output/best_model/inference",
)
# → real boxes with scores 0.3–0.97

Observed symptoms

Source Logits max (approx) Max sigmoid score Boxes @ 0.25
Hub PP-DocLayoutV3_safetensors ~+1.7 ~0.84 many
Converted fine-tuned safetensors ~-3.0 ~0.05 0
Paddle static best_model/inference n/a high many

Loading reports 0 missing / unexpected keys; shapes match. Failures are silent.

Root-cause evidence (weight layout)

Compared best_model.pdparams vs converted model.safetensors:

  1. transformer.score_head.weight [256, 7] → model.enc_score_head.weight [7, 256]

    • File matches np.reshape(7, 256), not np.transpose.
    • Correct HF nn.Linear layout should be transpose ([in, out] → [out, in]).
    • Current WeightConverter._preprocess_tensors intends to transpose (_should_transpose), but the written file for this head matched reshape.
  2. Attention out_proj.weight (square [256, 256])

    • File matches paddle layout without transpose, while _should_transpose(...out_proj.weight) is True — inconsistent with intended HF layout.
  3. Fused in_proj → q/k/v split

    • Reconstructing via transpose-then-split does not match tensors stored in the converted safetensors (large maxdiff).

decoder.class_embed being absent from the file is expected (HF _tied_weights_keys: decoder.class_embed ↔ enc_score_head). That is not the root cause.

Related code

  • paddlex/modules/base/weight_converter.py — WeightConverter, _preprocess_tensors, _should_transpose
  • paddlex/modules/base/utils/pdparams2safetensors/mapping.py — RTDETR_MAPPING (registered for PP-DocLayoutV3)
  • paddlex/modules/base/utils/pdparams2safetensors/model_config.py — default 25-class id2label written into export config.json

Expected

  1. Converted model.safetensors should be numerically equivalent (after documented paddle→HF transforms) to the source .pdparams, and produce comparable detections under transformers / HF as paddle static.
  2. Export should either:
    • derive num_labels / id2label / inference.yml label_list from the training checkpoint / dataset config, or
    • document clearly that users must rewrite the default 25-class metadata for fine-tunes.
  3. Prefer failing loud (checksum / shape / smoke-test forward) over silent empty predictions when Linear layout is wrong.

Impact

Fine-tuned PP-DocLayoutV3 cannot be used via HF / engine="transformers" after pdparams2safetensors without a manual weight fix or sticking to paddle static export (inference dir).

Workaround

Use paddle static inference weights (.../best_model/inference) for production until conversion is fixed. Official hub safetensors remain OK for the pretrained 25-class model.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions