[Model] Serve DeepSeek-OCR-2 with its official 768px local-crop geometry (#38996)
This commit is contained in:
@@ -23,7 +23,8 @@ from sglang.srt.sampling.custom_logit_processor import (
|
||||
DeepseekOCRImage = Union[Image.Image, torch.Tensor]
|
||||
|
||||
BASE_SIZE = 1024
|
||||
IMAGE_SIZE = 640
|
||||
IMAGE_SIZE = 640 # DeepSeek-OCR local crop; OCR-2 uses OCR2_IMAGE_SIZE
|
||||
OCR2_IMAGE_SIZE = 768
|
||||
CROP_MODE = True
|
||||
MIN_CROPS = 2
|
||||
MAX_CROPS = 6 # max:9; If your GPU memory is small, it is recommended to set it to 6.
|
||||
@@ -218,7 +219,11 @@ def find_closest_aspect_ratio(aspect_ratio, target_ratios, width, height, image_
|
||||
|
||||
|
||||
def dynamic_preprocess(
|
||||
image, min_num=MIN_CROPS, max_num=MAX_CROPS, image_size=640, use_thumbnail=False
|
||||
image,
|
||||
min_num=MIN_CROPS,
|
||||
max_num=MAX_CROPS,
|
||||
image_size=IMAGE_SIZE,
|
||||
use_thumbnail=False,
|
||||
):
|
||||
orig_width, orig_height = get_image_size(image)
|
||||
aspect_ratio = orig_width / orig_height
|
||||
@@ -263,6 +268,28 @@ def dynamic_preprocess(
|
||||
return processed_images, target_aspect_ratio
|
||||
|
||||
|
||||
def is_ocr2_config(config) -> bool:
|
||||
"""Whether a checkpoint is DeepSeek-OCR-2.
|
||||
|
||||
Both checkpoints ship identical processor configs, so identity comes from the
|
||||
model config: the DeepEncoder V2 vision encoder, or its 896-dim projector.
|
||||
Both lookups are guarded because the projector clause is what covers derived
|
||||
checkpoints that drop `model_name` -- reading it unguarded would raise before
|
||||
that clause is reached.
|
||||
"""
|
||||
vision_config = getattr(config, "vision_config", None)
|
||||
projector_config = getattr(config, "projector_config", None)
|
||||
return (
|
||||
str(getattr(vision_config, "model_name", "")).lower() == "deepencoderv2"
|
||||
or getattr(projector_config, "input_dim", None) == 896
|
||||
)
|
||||
|
||||
|
||||
def local_crop_size(config) -> int:
|
||||
"""Local-crop pixel size: 768 for OCR-2, 640 for DeepSeek-OCR."""
|
||||
return OCR2_IMAGE_SIZE if is_ocr2_config(config) else IMAGE_SIZE
|
||||
|
||||
|
||||
class DeepseekOCRProcessor(ProcessorMixin):
|
||||
tokenizer_class = ("LlamaTokenizer", "LlamaTokenizerFast")
|
||||
attributes = ["tokenizer"]
|
||||
@@ -287,6 +314,8 @@ class DeepseekOCRProcessor(ProcessorMixin):
|
||||
):
|
||||
|
||||
self.candidate_resolutions = candidate_resolutions
|
||||
# The checkpoint config carries the *global* base here, not the local
|
||||
# crop; the SGLang processor patches the real crop size per model.
|
||||
self.image_size = candidate_resolutions[0][0]
|
||||
self.patch_size = patch_size
|
||||
self.image_mean = image_mean
|
||||
@@ -543,18 +572,23 @@ class DeepseekOCRProcessor(ProcessorMixin):
|
||||
img_w, img_h = get_image_size(image)
|
||||
image_shapes.append((img_w, img_h))
|
||||
|
||||
if img_w <= 640 and img_h <= 640:
|
||||
# Both official processors threshold on their own crop size (640 for
|
||||
# OCR-1, 768 for OCR-2), which is what `image_size` holds here.
|
||||
if img_w <= self.image_size and img_h <= self.image_size:
|
||||
crop_ratio = [1, 1]
|
||||
else:
|
||||
if cropping:
|
||||
images_crop_raw, crop_ratio = dynamic_preprocess(
|
||||
image, image_size=IMAGE_SIZE
|
||||
image, image_size=self.image_size
|
||||
)
|
||||
else:
|
||||
crop_ratio = [1, 1]
|
||||
|
||||
"""process the global view"""
|
||||
if self.image_size <= 640 and not cropping:
|
||||
# Upstream compares against the model's own crop constant, which is
|
||||
# exactly what `image_size` holds, so the test is always true and the
|
||||
# guard reduces to `not cropping`.
|
||||
if not cropping:
|
||||
image = resize_image(image, (self.image_size, self.image_size))
|
||||
|
||||
global_view = pad_image(
|
||||
|
||||
@@ -31,7 +31,7 @@ import transformers
|
||||
from torch import Tensor, nn
|
||||
from transformers.models.vitdet.modeling_vitdet import get_rel_pos
|
||||
|
||||
from sglang.srt.configs.deepseek_ocr import DeepseekVLV2Config
|
||||
from sglang.srt.configs.deepseek_ocr import DeepseekVLV2Config, is_ocr2_config
|
||||
from sglang.srt.layers.quantization import QuantizationConfig
|
||||
from sglang.srt.managers.mm_utils import (
|
||||
MultiModalityDataPaddingPatternMultimodalTokens,
|
||||
@@ -1420,18 +1420,12 @@ def build_qwen2_decoder_as_encoder(
|
||||
return decoder_as_encoder
|
||||
|
||||
|
||||
def _is_ocr2(config: DeepseekVLV2Config) -> bool:
|
||||
return (
|
||||
str(getattr(config.vision_config, "model_name", "")).lower() == "deepencoderv2"
|
||||
or getattr(config.projector_config, "input_dim", None) == 896
|
||||
)
|
||||
|
||||
|
||||
class DeepseekOCRForCausalLM(nn.Module):
|
||||
@staticmethod
|
||||
def shared_experts_fusion_disable_reason(hf_config, quant_config):
|
||||
text_config = hf_config.text_config
|
||||
if _is_ocr2(hf_config) or not (
|
||||
# Class-level hook: called before the model is built, so no `self.is_ocr2` yet.
|
||||
if is_ocr2_config(hf_config) or not (
|
||||
text_config.topk_method == "noaux_tc" or text_config.use_mla
|
||||
):
|
||||
# Those branches build the dense DeepseekForCausalLM, which has no
|
||||
@@ -1455,7 +1449,7 @@ class DeepseekOCRForCausalLM(nn.Module):
|
||||
self.vision_config = config.vision_config
|
||||
self.projector_config = config.projector_config
|
||||
self.text_config = config.text_config
|
||||
self.is_ocr2 = _is_ocr2(config)
|
||||
self.is_ocr2 = is_ocr2_config(config)
|
||||
n_embed = getattr(self.projector_config, "n_embed", 1280)
|
||||
|
||||
self.tile_tag = config.tile_tag
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import List, Union
|
||||
|
||||
from sglang.srt.configs.deepseek_ocr import is_ocr2_config, local_crop_size
|
||||
from sglang.srt.managers.schedule_batch import MultimodalProcessorOutput
|
||||
from sglang.srt.models.deepseek_ocr import DeepseekOCRForCausalLM
|
||||
from sglang.srt.multimodal.processors.base_processor import (
|
||||
@@ -8,19 +9,24 @@ from sglang.srt.multimodal.processors.base_processor import (
|
||||
)
|
||||
|
||||
|
||||
def apply_ocr_geometry(processor, hf_config) -> None:
|
||||
"""Patch a checkpoint's local-crop geometry onto an already-built HF processor.
|
||||
|
||||
`image_size` is overwritten unconditionally: neither checkpoint carries the
|
||||
crop size (see `local_crop_size`), so a value in `processor_config.json` would
|
||||
not survive.
|
||||
"""
|
||||
processor.ocr2_mode = is_ocr2_config(hf_config)
|
||||
processor.image_size = local_crop_size(hf_config)
|
||||
|
||||
|
||||
class DeepseekOCRProcessor(BaseMultimodalProcessor):
|
||||
models = [DeepseekOCRForCausalLM]
|
||||
|
||||
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
||||
_processor.image_size = 640
|
||||
_processor.ocr2_mode = (
|
||||
str(
|
||||
getattr(getattr(hf_config, "vision_config", None), "model_name", "")
|
||||
).lower()
|
||||
== "deepencoderv2"
|
||||
or getattr(getattr(hf_config, "projector_config", None), "input_dim", None)
|
||||
== 896
|
||||
)
|
||||
# The shared processor config's candidate_resolutions is the *global* base, so
|
||||
# the crop size has to come from the model identity instead.
|
||||
apply_ocr_geometry(_processor, hf_config)
|
||||
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
||||
self.mm_tokens = MultimodalSpecialTokens(
|
||||
image_token="<image>", image_token_id=self._processor.image_token_id
|
||||
|
||||
Reference in New Issue
Block a user