[Model] Serve DeepSeek-OCR-2 with its official 768px local-crop geometry (#38996)

This commit is contained in:
chaijiacheng888
2026-09-20 14:17:21 +08:00
committed by GitHub
parent 031bff5dd3
commit 2d216a11f8
4 changed files with 140 additions and 24 deletions
@@ -0,0 +1,82 @@
"""DeepSeek-OCR-2 must run 768px local crops, not the 640px ones inherited from
DeepSeek-OCR: a 768px crop expands to (768 // 16 // 4) ** 2 == 144 visual tokens,
exactly the length of the tuned query_768 table in Qwen2Decoder2Encoder, while
640px yields 100 and falls back to interpolation.
Neither checkpoint carries the crop size -- both ship identical processor configs
whose candidate_resolutions=[[1024, 1024]] is the *global* base -- so it is
selected from the model config and patched onto the processor. The in-processor
thresholds that consume `image_size` need a real tokenizer, so they are only
observable end to end.
"""
import unittest
from types import SimpleNamespace
from PIL import Image
from sglang.srt.configs.deepseek_ocr import (
IMAGE_SIZE,
OCR2_IMAGE_SIZE,
dynamic_preprocess,
local_crop_size,
)
from sglang.srt.multimodal.processors.deepseek_ocr import apply_ocr_geometry
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=60, suite="base-a-test-cpu")
def _hf_config(vision_model_name: str, projector_input_dim) -> SimpleNamespace:
return SimpleNamespace(
vision_config=SimpleNamespace(model_name=vision_model_name),
projector_config=SimpleNamespace(input_dim=projector_input_dim),
)
def _ocr2_config() -> SimpleNamespace:
"""DeepSeek-OCR-2: DeepEncoder V2 vision encoder, 896-dim projector."""
return _hf_config("deepencoderv2", 896)
def _ocr1_config() -> SimpleNamespace:
"""DeepSeek-OCR: deeplip_b_l vision encoder, 2048-dim projector."""
return _hf_config("deeplip_b_l", 2048)
class TestDeepseekOcrGeometry(CustomTestCase):
def test_local_crop_size_is_768_for_ocr2_and_640_for_ocr1(self):
# OCR-2 must not be served with OCR-1's 640px crops; OCR-1 must stay on 640.
self.assertEqual(local_crop_size(_ocr2_config()), 768)
self.assertEqual(local_crop_size(_ocr2_config()), OCR2_IMAGE_SIZE)
self.assertEqual(local_crop_size(_ocr1_config()), 640)
self.assertEqual(local_crop_size(_ocr1_config()), IMAGE_SIZE)
def test_processor_geometry_wiring(self):
# Assert the patching itself, so a regression in the call site is caught too.
stub = SimpleNamespace()
apply_ocr_geometry(stub, _ocr2_config())
self.assertEqual(stub.image_size, 768)
self.assertTrue(stub.ocr2_mode)
apply_ocr_geometry(stub, _ocr1_config())
self.assertEqual(stub.image_size, 640)
self.assertFalse(stub.ocr2_mode)
def test_dynamic_preprocess_crop_px_follows_image_size(self):
# `tokenize_with_images` passes `image_size` through, so the crop pixels
# follow whichever crop size was selected above.
wide = Image.new("RGB", (3072, 1024)) # aspect 3:1 -> 3x1 tile grid
for image_size, expected_size in ((768, 768), (640, 640)):
crops, ratio = dynamic_preprocess(
image=wide, image_size=image_size, min_num=2, max_num=6
)
self.assertEqual(ratio, (3, 1))
self.assertEqual(len(crops), 3)
self.assertTrue(
all(crop.size == (expected_size, expected_size) for crop in crops)
)
if __name__ == "__main__":
unittest.main()