Blank/uniform page produces a fabricated document instead of empty output (transformers path only)

#45
by evo42 - opened

On a completely blank page, LightOnOCR-2-1B does not return empty output — it deterministically generates a generic fake document (# Document Title / ## Section 1 / - Item 1 …), then degenerates into repeating $y = \sin(x)$ until it hits max_new_tokens. In my case that is 7,885 characters of pure fabrication for ~450 s of compute on a page containing nothing.

This matters in production document pipelines because blank pages are routine — separator sheets, backs of duplex scans — and the failure is silent: the output is plausibly structured markdown that downstream indexing will happily ingest.

Reproduction

No external files needed:

import torch
from PIL import Image
from transformers import LightOnOcrForConditionalGeneration, LightOnOcrProcessor

device, dtype = "cpu", torch.float32
model = LightOnOcrForConditionalGeneration.from_pretrained(
    "lightonai/LightOnOCR-2-1B", dtype=dtype).to(device).eval()
processor = LightOnOcrProcessor.from_pretrained("lightonai/LightOnOCR-2-1B")

image = Image.new("RGB", (1090, 1540), "white")   # a completely blank page

conversation = [{"role": "user", "content": [{"type": "image", "image": image}]}]
inputs = processor.apply_chat_template(conversation, add_generation_prompt=True,
                                       tokenize=True, return_dict=True, return_tensors="pt")
inputs = {k: (v.to(device=device, dtype=dtype) if v.is_floating_point() else v.to(device))
          for k, v in inputs.items()}
with torch.no_grad():
    out = model.generate(**inputs, max_new_tokens=64, do_sample=False)
print(processor.decode(out[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True))

Actual:

# Document Title

## Section 1

### Subsection 1.1

- Item 1
- Item 2
- Item 3
...

Left to run, it continues into $y = \sin(x)$ repeated ~473×.

Expected: empty output, or the "no visible content" response the model already gives on nearly-blank pages (see below).

What I ruled out

variant result
transformers, image resized to 1540px longest side fabricates
transformers, image at native size (no resize) identical fabrication
transformers on CPU fabricates
transformers on MPS fabricates
llama.cpp GGUF Q4_K_M ✅ correct refusal
llama.cpp GGUF F16 ✅ correct refusal

The F16 GGUF control is the interesting one: same weights, same precision, no quantization noise — and it correctly answers "There is no visible content in the provided image." Combined with the failure being device-independent and resize-independent, this looks like it sits in the transformers-side implementation rather than the weights — plausibly LightOnOcrProcessor's handling of a constant-valued image (a uniform image has zero variance, so normalization produces a degenerate input).

Any real content defeats it

Which supports that reading:

input output
pure white page 7,885 chars fabricated
white + one line of text ✅ reads the line correctly
white + faint noise (~0.1% of pixels) "There is no visible content in the provided image"

So the model has a correct refusal behaviour — it just isn't reached when the input is perfectly uniform.

Environment

  • transformers 5.9.0, torch 2.12.0, Pillow 12.2.0
  • Python 3.11.13, macOS 26.5.2 arm64; reproduced on both cpu and mps
  • attn_implementation: sdpa (default)
  • control: llama.cpp b77d646, F16 + Q4_K_M GGUF converted from the same checkpoint

Workaround

For anyone hitting this: skip pages whose non-white pixel fraction is below ~1e-5 before calling the model. On my corpus that recovered ~14 minutes of wasted compute across two pages, with no false positives on real content.

Thanks for releasing the model — the German-language quality has been excellent in my testing, which is why this edge case stood out.

LightOn AI org

Hello,
Thanks for the feedback.
Empty pages are specifically handled, and in most cases the model correctly outputs nothing, though some edge cases remain. We’re actively working on improving this.
I also noticed you’re using quantized weights through llama.cpp, which in my testing tends to have more looping issues than vLLM and could be contributing here.

Sign up or log in to comment