Skip to content

Commit 894bb27

Browse files
authored
mtmd: model: unlimited-ocr: converter + parity test (ggml-org#24969)
1 parent fb40104 commit 894bb27

3 files changed

Lines changed: 61 additions & 8 deletions

File tree

conversion/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"DbrxForCausalLM": "dbrx",
4747
"DeciLMForCausalLM": "deci",
4848
"DeepseekForCausalLM": "deepseek",
49+
"DeepseekOCRForCausalLM": "deepseek",
4950
"DeepseekV2ForCausalLM": "deepseek",
5051
"DeepseekV3ForCausalLM": "deepseek",
5152
"DeepseekV32ForCausalLM": "deepseek",
@@ -233,6 +234,7 @@
233234
"UMT5ForConditionalGeneration": "t5",
234235
"UMT5Model": "t5",
235236
"UltravoxModel": "ultravox",
237+
"UnlimitedOCRForCausalLM": "deepseek",
236238
"VLlama3ForCausalLM": "llama",
237239
"VoxtralForConditionalGeneration": "llama",
238240
"WavTokenizerDec": "wavtokenizer",
@@ -299,6 +301,7 @@
299301
"StepVLForConditionalGeneration": "step3",
300302
"Step3p7ForConditionalGeneration": "step3",
301303
"UltravoxModel": "ultravox",
304+
"UnlimitedOCRForCausalLM": "deepseek",
302305
"VoxtralForConditionalGeneration": "ultravox",
303306
"YoutuVLForConditionalGeneration": "youtuvl",
304307
}

conversion/deepseek.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .qwen import QwenModel
1515

1616

17-
@ModelBase.register("DeepseekOCRForCausalLM")
17+
@ModelBase.register("DeepseekOCRForCausalLM", "UnlimitedOCRForCausalLM")
1818
class DeepseekOCRVisionModel(MmprojModel):
1919
def __init__(self, *args, **kwargs):
2020
super().__init__(*args, **kwargs)
@@ -205,6 +205,8 @@ def prepare_tensors(self):
205205
@ModelBase.register(
206206
"DeepseekV2ForCausalLM",
207207
"DeepseekV3ForCausalLM",
208+
"DeepseekOCRForCausalLM",
209+
"UnlimitedOCRForCausalLM",
208210
"KimiVLForConditionalGeneration",
209211
"KimiK25ForConditionalGeneration",
210212
"YoutuForCausalLM",
@@ -224,7 +226,7 @@ def __init__(self, *args, **kwargs):
224226
self.origin_hf_arch = hparams.get('architectures', [None])[0]
225227

226228
# special handling for Deepseek OCR
227-
if self.origin_hf_arch in ("DeepseekOCRForCausalLM", "DeepseekOCR2ForCausalLM"):
229+
if self.origin_hf_arch in ("DeepseekOCRForCausalLM", "DeepseekOCR2ForCausalLM", "UnlimitedOCRForCausalLM"):
228230
self.model_arch = gguf.MODEL_ARCH.DEEPSEEK2OCR
229231
self.gguf_writer.arch = gguf.MODEL_ARCH_NAMES[self.model_arch]
230232
self.gguf_writer.add_architecture()
@@ -350,6 +352,12 @@ def set_gguf_parameters(self):
350352

351353
self.gguf_writer.add_rope_dimension_count(hparams["qk_rope_head_dim"])
352354

355+
# Unlimited-OCR sliding window; written for metadata, the decoder ignores it (full MHA)
356+
if is_ocr:
357+
sliding_window = hparams.get("sliding_window_size") or hparams.get("sliding_window")
358+
if sliding_window:
359+
self.gguf_writer.add_sliding_window(sliding_window)
360+
353361
if (rope_mscale_all := self.rope_parameters.get("mscale_all_dim")) is not None:
354362
# [TAG_DEEPSEEK2_YARN_LOG_MUL_FIX]
355363
# note: for legacy reasons, this is not consistent with the other usages of self.gguf_writer.add_rope_scaling_yarn_log_mul

tools/mtmd/tests/test-deepseek-ocr.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import argparse
1111
import logging
12+
import re
1213
import subprocess
1314
import sys
1415
import unicodedata
@@ -28,6 +29,12 @@ class ModelSpec:
2829
mmproj_arg: str
2930
model_default: str
3031
mmproj_default: str
32+
prompt: str = "Free OCR. "
33+
n_predict: int = 512
34+
n_ctx: int | None = None
35+
# Unlimited-OCR's "document parsing" prompt emits <|det|> grounding markup that
36+
# the HF reference strips in result.md; drop it before scoring to match.
37+
strip_grounding: bool = False
3138

3239

3340
@dataclass
@@ -63,6 +70,20 @@ def chrf_min(self) -> float:
6370
model_default="gguf_models/deepseek-ai/deepseek-ocr-2-bf16.gguf",
6471
mmproj_default="gguf_models/deepseek-ai/mmproj-deepseek-ocr-2-bf16.gguf",
6572
),
73+
"unlimited": ModelSpec(
74+
key="unlimited", label="Unlimited-OCR",
75+
model_arg="--llama-model-unlimited", mmproj_arg="--mmproj-unlimited",
76+
model_default="gguf_models/baidu/unlimited-ocr-bf16.gguf",
77+
mmproj_default="gguf_models/baidu/mmproj-unlimited-ocr-bf16.gguf",
78+
# "Free OCR." immediately emits EOS on this checkpoint; the HF reference
79+
# (demo/unlimited_ocr_scores.py) uses "document parsing.", which grounds.
80+
prompt="document parsing.",
81+
# Grounding emits ~3x the tokens of plain OCR, so it needs a larger budget
82+
# and context to reach the article body the ground truth covers.
83+
n_predict=4096,
84+
n_ctx=16384,
85+
strip_grounding=True,
86+
),
6687
}
6788

6889
CASES = [
@@ -82,9 +103,26 @@ def chrf_min(self) -> float:
82103
# is one pixel off and lands at ~0.69 instead.
83104
hf_cer=0.7761, hf_chrf=28.70, cer_tol=0.12, chrf_tol=8.0,
84105
),
106+
TestCase(
107+
model_key="unlimited", label="single-view scan",
108+
image="tools/mtmd/test-1.jpeg",
109+
ground_truth="tools/mtmd/tests/test-1-ground-truth.txt",
110+
# HF reference: Unlimited-OCR scoring (gundam, bf16) on this image/ground-truth.
111+
# Decoder runs full MHA, not R-SWA; the band absorbs that gap + bf16 variance.
112+
hf_cer=0.1869, hf_chrf=75.23, cer_tol=0.06, chrf_tol=6.0,
113+
),
85114
]
86115

87116

117+
GROUNDING_TAG_RE = re.compile(r"<\|(ref|det)\|>.*?<\|/\1\|>", re.DOTALL)
118+
119+
120+
def strip_grounding(text: str) -> str:
121+
"""Drop <|ref|>..<|/ref|> / <|det|>..<|/det|> grounding markup, matching the
122+
cleaned result.md the HF reference scores against."""
123+
return GROUNDING_TAG_RE.sub("", text)
124+
125+
88126
def arg_dest(flag: str) -> str:
89127
return flag.lstrip("-").replace("-", "_")
90128

@@ -129,19 +167,19 @@ def compute_chrf(expected: str, ocr_out: str) -> float:
129167
return CHRF().sentence_score(ocr_out, [expected]).score
130168

131169

132-
def run_mtmd_cli(model_path, mmproj_path, image_path, bin_path) -> str:
170+
def run_mtmd_cli(spec: "ModelSpec", model_path, mmproj_path, image_path, bin_path) -> str:
133171
"""Run mtmd-cli on the image and return its output."""
134172
cmd = [
135173
str(bin_path),
136174
"-m", str(model_path),
137175
"--mmproj", str(mmproj_path),
138176
"--image", str(image_path),
139-
"-p", "Free OCR. ",
177+
"-p", spec.prompt,
140178
"--chat-template", "deepseek-ocr",
141179
"--temp", "0",
142180
"--flash-attn", "off", # match the HF "eager" attention reference
143181
"--no-warmup",
144-
"-n", "512", # cap loops on hard images (KV would otherwise fill)
182+
"-n", str(spec.n_predict), # cap loops on hard images (KV would otherwise fill)
145183
# HF decodes with no_repeat_ngram_size; llama.cpp's analog is DRY.
146184
# Default DRY breakers include "\n", so they are cleared below.
147185
"--dry-multiplier", "0.8",
@@ -150,6 +188,8 @@ def run_mtmd_cli(model_path, mmproj_path, image_path, bin_path) -> str:
150188
"--dry-penalty-last-n", "-1",
151189
"--dry-sequence-breaker", "none",
152190
]
191+
if spec.n_ctx is not None:
192+
cmd += ["-c", str(spec.n_ctx)]
153193
logger.debug(f" command: {' '.join(cmd)}")
154194

155195
try:
@@ -164,6 +204,8 @@ def run_mtmd_cli(model_path, mmproj_path, image_path, bin_path) -> str:
164204
raise RuntimeError(f"llama-mtmd-cli failed with code {result.returncode}")
165205

166206
output = result.stdout.decode("utf-8", errors="replace").strip()
207+
if spec.strip_grounding:
208+
output = strip_grounding(output)
167209
if not output:
168210
raise RuntimeError("llama-mtmd-cli produced no output on stdout")
169211
logger.info(f" output: {len(output)} chars")
@@ -193,7 +235,7 @@ def evaluate(case: "TestCase", expected: str, ocr_out: str) -> bool:
193235

194236
logger.info("")
195237
logger.info("=" * 60)
196-
logger.info("Free OCR evaluation:")
238+
logger.info("OCR evaluation:")
197239
logger.info("=" * 60)
198240
logger.info(f" CER {cer:>7.4f} (HF {case.hf_cer:.4f}, <= {case.cer_max:>7.4f} -> {verdict(cer_pass)})")
199241
logger.info(f" chrF (0-100) {chrf:>7.2f} (HF {case.hf_chrf:.2f}, >= {case.chrf_min:>7.2f} -> {verdict(chrf_pass)})")
@@ -269,9 +311,9 @@ def main() -> int:
269311
expected = read_expected_text(ground_truth)
270312
logger.info(f" Image: {case.image}")
271313
logger.info(f" Expected text: {len(expected)} chars")
272-
logger.info(" Running llama.cpp 'Free OCR'")
314+
logger.info(f" Running llama.cpp prompt {model_spec.prompt!r}")
273315
try:
274-
ocr_out = run_mtmd_cli(model, mmproj, image, binary)
316+
ocr_out = run_mtmd_cli(model_spec, model, mmproj, image, binary)
275317
except RuntimeError as e:
276318
logger.error(f" Error: {e}")
277319
results[title] = False

0 commit comments

Comments
 (0)