model: support DeepSeek-OCR-2 (#17897)

This commit is contained in:
baonudesifeizhai
2026-01-29 20:49:51 -05:00
committed by GitHub
parent 2cd2c3118d
commit 84ab611af8
9 changed files with 620 additions and 142 deletions

View File

@@ -0,0 +1,54 @@
# DeepSeek OCR (OCR-1 / OCR-2)
DeepSeek OCR models are multimodal (image + text) models for OCR and document understanding.
## Launch server
```shell
python -m sglang.launch_server \
--model-path deepseek-ai/DeepSeek-OCR-2 \
--trust-remote-code \
--host 0.0.0.0 \
--port 30000
```
> You can replace `deepseek-ai/DeepSeek-OCR-2` with `deepseek-ai/DeepSeek-OCR`.
## Prompt examples
Recommended prompts from the model card:
```
<image>
<|grounding|>Convert the document to markdown.
```
```
<image>
Free OCR.
```
## OpenAI-compatible request example
```python
import requests
url = "http://localhost:30000/v1/chat/completions"
data = {
"model": "deepseek-ai/DeepSeek-OCR-2",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<image>\n<|grounding|>Convert the document to markdown."},
{"type": "image_url", "image_url": {"url": "https://example.com/your_image.jpg"}},
],
}
],
"max_tokens": 512,
}
response = requests.post(url, json=data)
print(response.text)
```

View File

@@ -33,6 +33,7 @@ Its core features include:
basic_usage/ollama_api.md
basic_usage/offline_engine_api.ipynb
basic_usage/native_api.ipynb
basic_usage/deepseek_ocr.md
basic_usage/sampling_params.md
basic_usage/popular_model_usage.rst

View File

@@ -30,6 +30,7 @@ in the GitHub search bar.
|----------------------------|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|
| **Qwen-VL** | `Qwen/Qwen3-VL-235B-A22B-Instruct` | Alibaba's vision-language extension of Qwen; for example, Qwen2.5-VL (7B and larger variants) can analyze and converse about image content. | |
| **DeepSeek-VL2** | `deepseek-ai/deepseek-vl2` | Vision-language variant of DeepSeek (with a dedicated image processor), enabling advanced multimodal reasoning on image and text inputs. | |
| **DeepSeek-OCR / OCR-2** | `deepseek-ai/DeepSeek-OCR-2` | OCR-focused DeepSeek models for document understanding and text extraction. | Use `--trust-remote-code`. |
| **Janus-Pro** (1B, 7B) | `deepseek-ai/Janus-Pro-7B` | DeepSeek's open-source multimodal model capable of both image understanding and generation. Janus-Pro employs a decoupled architecture for separate visual encoding paths, enhancing performance in both tasks. | |
| **MiniCPM-V / MiniCPM-o** | `openbmb/MiniCPM-V-2_6` | MiniCPM-V (2.6, ~8B) supports image inputs, and MiniCPM-o adds audio/video; these multimodal LLMs are optimized for end-side deployment on mobile/edge devices. | |
| **Llama 3.2 Vision** (11B) | `meta-llama/Llama-3.2-11B-Vision-Instruct` | Vision-enabled variant of Llama 3 (11B) that accepts image inputs for visual question answering and other multimodal tasks. | |

View File

@@ -196,6 +196,7 @@ class DeepseekOCRProcessor(ProcessorMixin):
sft_format: str = "deepseek",
mask_prompt: bool = True,
ignore_id: int = -100,
ocr2_mode: bool = False,
**kwargs,
):
@@ -243,6 +244,7 @@ class DeepseekOCRProcessor(ProcessorMixin):
self.sft_format = sft_format
self.mask_prompt = mask_prompt
self.ignore_id = ignore_id
self.ocr2_mode = ocr2_mode
super().__init__(
tokenizer,
@@ -359,6 +361,13 @@ class DeepseekOCRProcessor(ProcessorMixin):
target_ids = torch.LongTensor(masked_tokenized_str)
has_images = len(images_list) > 0
has_local_crops = False
if len(images_spatial_crop) > 0:
has_local_crops = any(
crop[0] > 1 or crop[1] > 1 for crop in images_spatial_crop
)
if len(images_list) == 0:
images = torch.zeros((1, 3, self.image_size, self.image_size))
else:
@@ -376,6 +385,8 @@ class DeepseekOCRProcessor(ProcessorMixin):
images_seq_mask=images_seq_mask,
images_spatial_crop=images_spatial_crop,
)
prepare.has_images = has_images
prepare.has_local_crops = has_local_crops
return prepare
@@ -481,15 +492,27 @@ class DeepseekOCRProcessor(ProcessorMixin):
(self.base_size // self.patch_size) / self.downsample_ratio
)
tokenized_image = (
[self.image_token_id] * num_queries_base + [self.image_token_id]
) * num_queries_base
tokenized_image += [self.image_token_id]
if num_width_tiles > 1 or num_height_tiles > 1:
tokenized_image += (
[self.image_token_id] * (num_queries * num_width_tiles)
+ [self.image_token_id]
) * (num_queries * num_height_tiles)
if self.ocr2_mode:
tokenized_image = []
if num_width_tiles > 1 or num_height_tiles > 1:
tokenized_image += [self.image_token_id] * (
num_queries * num_width_tiles * num_queries * num_height_tiles
)
tokenized_image += [self.image_token_id] * (
num_queries_base * num_queries_base
)
# One extra token for the view separator.
tokenized_image += [self.image_token_id]
else:
tokenized_image = (
[self.image_token_id] * num_queries_base + [self.image_token_id]
) * num_queries_base
tokenized_image += [self.image_token_id]
if num_width_tiles > 1 or num_height_tiles > 1:
tokenized_image += (
[self.image_token_id] * (num_queries * num_width_tiles)
+ [self.image_token_id]
) * (num_queries * num_height_tiles)
tokenized_str += tokenized_image
images_seq_mask += [True] * len(tokenized_image)

View File

@@ -1050,7 +1050,12 @@ def _get_and_verify_dtype(
) -> torch.dtype:
# NOTE: getattr(config, "torch_dtype", torch.float32) is not correct
# because config.torch_dtype can be None.
config_dtype = getattr(config, "dtype", None)
if isinstance(config, dict):
config_dtype = config.get("dtype", None) or config.get("torch_dtype", None)
model_type = config.get("model_type", "")
else:
config_dtype = getattr(config, "dtype", None)
model_type = getattr(config, "model_type", "")
if isinstance(config_dtype, str):
config_dtype = _STR_DTYPE_TO_TORCH_DTYPE.get(config_dtype, None)
if config_dtype is None:
@@ -1060,11 +1065,11 @@ def _get_and_verify_dtype(
dtype = dtype.lower()
if dtype == "auto":
if config_dtype == torch.float32:
if config.model_type.startswith("gemma"):
if config.model_type == "gemma":
if model_type.startswith("gemma"):
if model_type == "gemma":
gemma_version = ""
else:
gemma_version = config.model_type[5]
gemma_version = model_type[5]
logger.info(
f"For Gemma {gemma_version}, we downcast float32 to bfloat16 instead "
"of float16 by default. Please specify `dtype` if you "

View File

@@ -59,14 +59,18 @@ def resolve_transformers_arch(model_config: ModelConfig, architectures: list[str
)
model_module = auto_modules["AutoModel"]
if model_config.model_impl == ModelImpl.TRANSFORMERS:
if not model_module.is_backend_compatible():
if hasattr(model_module, "is_backend_compatible") and (
not model_module.is_backend_compatible()
):
raise ValueError(
f"The Transformers implementation of {arch} is not "
"compatible with SGLang."
)
architectures[i] = "TransformersForCausalLM"
if model_config.model_impl == ModelImpl.AUTO:
if not model_module.is_backend_compatible():
if hasattr(model_module, "is_backend_compatible") and (
not model_module.is_backend_compatible()
):
raise ValueError(
f"{arch} has no SGlang implementation and the Transformers "
"implementation is not compatible with SGLang."

View File

@@ -24,6 +24,7 @@ from typing import Iterable, List, Optional, Set, Tuple, Type, TypeAlias, Union
import torch
import torch.nn.functional as F
import transformers
from torch import Tensor, nn
from transformers.models.vitdet.modeling_vitdet import get_rel_pos
@@ -702,6 +703,7 @@ class ImageEncoderViT(nn.Module):
rel_pos_zero_init: bool = True,
window_size: int = 0,
global_attn_indexes: Tuple[int, ...] = (),
net_3_out_channels: int = 1024,
) -> None:
"""
Args:
@@ -776,7 +778,7 @@ class ImageEncoderViT(nn.Module):
self.net_2 = nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1, bias=False)
self.net_3 = nn.Conv2d(
512, 1024, kernel_size=3, stride=2, padding=1, bias=False
512, net_3_out_channels, kernel_size=3, stride=2, padding=1, bias=False
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
@@ -800,6 +802,7 @@ def _build_sam(
encoder_num_heads,
encoder_global_attn_indexes,
checkpoint=None,
net_3_out_channels: int = 1024,
):
prompt_embed_dim = 256
image_size = 1024
@@ -817,6 +820,7 @@ def _build_sam(
global_attn_indexes=encoder_global_attn_indexes,
window_size=14,
out_chans=prompt_embed_dim,
net_3_out_channels=net_3_out_channels,
)
image_encoder.eval()
if checkpoint is not None:
@@ -828,13 +832,14 @@ def _build_sam(
return image_encoder
def build_sam_vit_b(checkpoint=None):
def build_sam_vit_b(checkpoint=None, net_3_out_channels: int = 1024):
return _build_sam(
encoder_embed_dim=768,
encoder_depth=12,
encoder_num_heads=12,
encoder_global_attn_indexes=[2, 5, 8, 11],
checkpoint=checkpoint,
net_3_out_channels=net_3_out_channels,
)
@@ -1146,6 +1151,257 @@ def build_clip_l():
)
class CustomQwen2Decoder(nn.Module):
"""Qwen2 decoder with mixed causal masking for OCR2 vision encoder."""
def __init__(
self,
decoder_layer: int = 24,
max_position_embeddings: int = 131072,
hidden_dimension: int = 896,
num_attention_heads: int = 14,
num_key_value_heads: int = 2,
intermediate_size: int = 4864,
vocab_size: int = 151936,
attn_implementation: str = "sdpa",
rms_norm_eps: float = 1e-6,
rope_theta: float = 1000000.0,
attention_dropout: float = 0.0,
hidden_act: str = "silu",
initializer_range: float = 0.02,
):
super().__init__()
if attn_implementation == "flash_attention_2":
raise ValueError(
"CustomQwen2Decoder does not support flash_attention_2; "
"use sdpa or eager."
)
Qwen2Model = getattr(transformers.models.qwen2.modeling_qwen2, "Qwen2Model")
Qwen2Config = getattr(transformers, "Qwen2Config")
config = Qwen2Config(
hidden_size=hidden_dimension,
num_hidden_layers=decoder_layer,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
intermediate_size=intermediate_size,
max_position_embeddings=max_position_embeddings,
vocab_size=vocab_size,
rms_norm_eps=rms_norm_eps,
rope_theta=rope_theta,
attention_dropout=attention_dropout,
hidden_act=hidden_act,
initializer_range=initializer_range,
_attn_implementation=attn_implementation,
)
self.model = self._create_custom_model(Qwen2Model, config)
del self.model.embed_tokens
def _create_custom_model(self, Qwen2Model, config):
class CustomQwen2ModelInner(Qwen2Model):
def forward(
self,
input_ids=None,
attention_mask=None,
position_ids=None,
past_key_values=None,
inputs_embeds=None,
token_type_ids=None,
use_cache=None,
output_attentions=None,
output_hidden_states=None,
return_dict=None,
cache_position=None,
):
self._current_token_type_ids = token_type_ids
return super().forward(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
cache_position=cache_position,
)
def _update_causal_mask(
self,
attention_mask,
input_tensor,
cache_position,
past_key_values,
output_attentions,
):
dtype, device = input_tensor.dtype, input_tensor.device
min_dtype = torch.finfo(dtype).min
batch_size, sequence_length = (
input_tensor.shape[0],
input_tensor.shape[1],
)
token_type_ids = getattr(self, "_current_token_type_ids", None)
if token_type_ids is None:
return super()._update_causal_mask(
attention_mask,
input_tensor,
cache_position,
past_key_values,
output_attentions,
)
causal_mask = self._create_custom_4d_mask(
sequence_length=sequence_length,
dtype=dtype,
device=device,
batch_size=batch_size,
token_type_ids=token_type_ids,
)
if attention_mask is not None and attention_mask.dim() == 2:
padding_mask = attention_mask[:, None, None, :].to(dtype=dtype)
padding_mask = (1.0 - padding_mask) * min_dtype
causal_mask = causal_mask + padding_mask
return causal_mask
def _create_custom_4d_mask(
self,
sequence_length,
dtype,
device,
batch_size,
token_type_ids,
):
min_dtype = torch.finfo(dtype).min
masks = []
for b in range(batch_size):
mask = torch.full(
(sequence_length, sequence_length),
fill_value=min_dtype,
dtype=dtype,
device=device,
)
type_ids = token_type_ids[b]
image_positions = (type_ids == 0).nonzero(as_tuple=True)[0]
text_positions = (type_ids == 1).nonzero(as_tuple=True)[0]
if len(image_positions) > 0:
mask[image_positions[:, None], image_positions] = 0.0
for i, text_pos in enumerate(text_positions):
if len(image_positions) > 0:
mask[text_pos, image_positions] = 0.0
mask[text_pos, text_positions[: i + 1]] = 0.0
masks.append(mask)
mask = torch.stack(masks, dim=0).unsqueeze(1)
return mask
return CustomQwen2ModelInner(config)
def forward(self, inputs_embeds, token_type_ids, attention_mask=None, **kwargs):
return self.model(
inputs_embeds=inputs_embeds,
token_type_ids=token_type_ids,
attention_mask=attention_mask,
**kwargs,
)
class Qwen2Decoder2Encoder(nn.Module):
"""Decoder-as-encoder for OCR2 vision tokens."""
def __init__(
self,
decoder_layer: int,
hidden_dimension: int,
num_attention_heads: int,
num_key_value_heads: int,
intermediate_size: int,
max_query: int,
):
super().__init__()
self.model = CustomQwen2Decoder(
decoder_layer=decoder_layer,
hidden_dimension=hidden_dimension,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
intermediate_size=intermediate_size,
attn_implementation="sdpa",
)
self.query_768 = nn.Embedding(144, hidden_dimension)
self.query_1024 = nn.Embedding(256, hidden_dimension)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.flatten(2).transpose(1, 2)
bs, n_query, _ = x.shape
if n_query == 144:
param_img = self.query_768.weight
elif n_query == 256:
param_img = self.query_1024.weight
else:
base = (
self.query_1024.weight
if n_query > self.query_768.num_embeddings
else self.query_768.weight
)
param_img = (
F.interpolate(
base.T.unsqueeze(0),
size=n_query,
mode="linear",
align_corners=False,
)
.squeeze(0)
.T
)
batch_query_imgs = param_img.unsqueeze(0).expand(bs, -1, -1)
x_combined = torch.cat([x, batch_query_imgs], dim=1)
token_type_ids = torch.cat(
[
torch.zeros(bs, n_query, dtype=torch.long, device=x.device),
torch.ones(bs, n_query, dtype=torch.long, device=x.device),
],
dim=1,
)
y = self.model(x_combined, token_type_ids)[0]
y = y[:, n_query:, :]
return y
def build_qwen2_decoder_as_encoder(
decoder_layer: int = 24,
hidden_dimension: int = 896,
num_attention_heads: int = 14,
num_key_value_heads: int = 2,
intermediate_size: int = 4864,
max_query: int = 400,
checkpoint=None,
):
decoder_as_encoder = Qwen2Decoder2Encoder(
decoder_layer=decoder_layer,
hidden_dimension=hidden_dimension,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
intermediate_size=intermediate_size,
max_query=max_query,
)
if checkpoint is not None:
state_dict = torch.load(checkpoint)
decoder_as_encoder.load_state_dict(state_dict, strict=True)
return decoder_as_encoder
class DeepseekOCRForCausalLM(nn.Module):
def __init__(
self,
@@ -1161,8 +1417,12 @@ class DeepseekOCRForCausalLM(nn.Module):
self.vision_config = config.vision_config
self.projector_config = config.projector_config
self.text_config = config.text_config
n_embed = 1280
self.is_ocr2 = (
str(getattr(self.vision_config, "model_name", "")).lower()
== "deepencoderv2"
or getattr(self.projector_config, "input_dim", None) == 896
)
n_embed = getattr(self.projector_config, "n_embed", 1280)
self.tile_tag = config.tile_tag
self.global_view_pos = config.global_view_pos
@@ -1171,48 +1431,136 @@ class DeepseekOCRForCausalLM(nn.Module):
embed_std = 1 / torch.sqrt(torch.tensor(n_embed, dtype=torch.float32))
if self.tile_tag == "2D":
# <|view_separator|>, <|\n|>
self.image_newline = nn.Parameter(torch.randn(n_embed) * embed_std)
self.view_seperator = nn.Parameter(torch.randn(n_embed) * embed_std)
if not self.is_ocr2:
self.image_newline = nn.Parameter(torch.randn(n_embed) * embed_std)
else:
raise ValueError(
f"Only 2D tile_tag is supported currently, got: {self.tile_tag}"
)
if self.text_config.topk_method == "noaux_tc":
self.model = DeepseekV3ForCausalLM(
config=config.text_config,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "language"),
)
elif not self.text_config.use_mla:
if not self.is_ocr2:
if self.text_config.topk_method == "noaux_tc":
self.model = DeepseekV3ForCausalLM(
config=config.text_config,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "language"),
)
elif not self.text_config.use_mla:
self.model = DeepseekForCausalLM(
config=config.text_config,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "language"),
)
else:
self.model = DeepseekV2ForCausalLM(
config=config.text_config,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "language"),
)
else:
# OCR2 language_config uses non-MLA attention (qk_* dims are 0).
# Use the non-MLA Deepseek model to avoid MLA-specific assumptions.
self.model = DeepseekForCausalLM(
config=config.text_config,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "language"),
)
if not self.is_ocr2:
self.sam_model = build_sam_vit_b()
self.vision_model = build_clip_l()
else:
self.model = DeepseekV2ForCausalLM(
config=config.text_config,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "language"),
projector_input_dim = getattr(self.projector_config, "input_dim", 896)
self.sam_model = build_sam_vit_b(net_3_out_channels=projector_input_dim)
self.qwen2_model = build_qwen2_decoder_as_encoder(
hidden_dimension=projector_input_dim
)
self.sam_model = build_sam_vit_b()
self.vision_model = build_clip_l()
n_embed = 1280
self.projector = MlpProjector(
projector_type="linear",
input_dim=2048,
projector_type=self.projector_config.projector_type,
input_dim=self.projector_config.input_dim,
n_embed=n_embed,
depth=self.projector_config.depth,
mlp_ratio=self.projector_config.mlp_ratio,
downsample_ratio=self.projector_config.downsample_ratio,
)
@staticmethod
def _collect_mm_flag(
items: List[MultimodalDataItem], flag_name: str
) -> Optional[List[bool]]:
values = []
for item in items:
value = getattr(item, flag_name, None)
if value is None:
return None
values.append(bool(value))
return values
def _encode_ocr2_features(self, images: torch.Tensor) -> torch.Tensor:
features = self.sam_model(images)
features = self.qwen2_model(features)
features = self.projector(features)
return features.view(-1, features.shape[-1])
def _encode_ocr1_features(self, images: torch.Tensor) -> torch.Tensor:
features_1 = self.sam_model(images)
features_2 = self.vision_model(images, features_1)
features = torch.cat(
(
features_2[:, 1:],
features_1.flatten(2).permute(0, 2, 1),
),
dim=-1,
)
return self.projector(features)
def _format_ocr1_global_features(self, features: torch.Tensor) -> torch.Tensor:
_, hw, n_dim = features.shape
h = w = int(hw**0.5)
features = features.view(h, w, n_dim)
features = torch.cat(
[features, self.image_newline[None, None, :].expand(h, 1, n_dim)],
dim=1,
)
return features.view(-1, n_dim)
def _format_ocr1_local_features(
self, features: torch.Tensor, crop_shape: torch.Tensor
) -> torch.Tensor:
_, hw2, n_dim2 = features.shape
h2 = w2 = int(hw2**0.5)
width_crop_num, height_crop_num = int(crop_shape[0]), int(crop_shape[1])
features = (
features.view(height_crop_num, width_crop_num, h2, w2, n_dim2)
.permute(0, 2, 1, 3, 4)
.reshape(height_crop_num * h2, width_crop_num * w2, n_dim2)
)
features = torch.cat(
[
features,
self.image_newline[None, None, :].expand(
height_crop_num * h2, 1, n_dim2
),
],
dim=1,
)
return features.view(-1, n_dim2)
def _parse_and_validate_image_input(self, **kwargs: object):
pixel_values = kwargs.pop("pixel_values", None)
images_spatial_crop = kwargs.pop("images_spatial_crop", None)
images_crop = kwargs.pop("images_crop", None)
has_images = kwargs.pop("has_images", None)
if pixel_values is None or torch.sum(pixel_values).item() == 0:
if pixel_values is None:
return None
if has_images is not None:
if not has_images:
return None
elif torch.sum(pixel_values).item() == 0:
return None
if pixel_values is not None:
@@ -1241,6 +1589,7 @@ class DeepseekOCRForCausalLM(nn.Module):
pixel_values: torch.Tensor,
images_crop: torch.Tensor,
images_spatial_crop: torch.Tensor,
has_local_crops: Optional[List[bool]] = None,
) -> NestedTensors:
# Pixel_values (global view): [n_image, batch_size, 3, height, width]
@@ -1250,108 +1599,61 @@ class DeepseekOCRForCausalLM(nn.Module):
images_in_this_batch = []
if not self.is_ocr2:
with torch.no_grad():
for jdx in range(images_spatial_crop.size(0)):
patches = images_crop[jdx][0].to(torch.bfloat16)
image_ori = pixel_values[jdx]
crop_shape = images_spatial_crop[jdx][0]
use_local_crops = (
has_local_crops[jdx]
if has_local_crops is not None
else torch.sum(patches).item() != 0
)
global_features = self._encode_ocr1_features(image_ori)
global_features = self._format_ocr1_global_features(global_features)
if use_local_crops:
local_features = self._encode_ocr1_features(patches)
local_features = self._format_ocr1_local_features(
local_features, crop_shape
)
global_local_features = torch.cat(
[
local_features,
global_features,
self.view_seperator[None, :],
],
dim=0,
)
else:
global_local_features = torch.cat(
[global_features, self.view_seperator[None, :]], dim=0
)
images_in_this_batch.append(global_local_features)
return images_in_this_batch
with torch.no_grad():
for jdx in range(images_spatial_crop.size(0)):
patches = images_crop[jdx][0].to(torch.bfloat16)
image_ori = pixel_values[jdx]
crop_shape = images_spatial_crop[jdx][0]
if torch.sum(patches).item() != 0:
local_features_1 = self.sam_model(patches)
local_features_2 = self.vision_model(patches, local_features_1)
local_features = torch.cat(
(
local_features_2[:, 1:],
local_features_1.flatten(2).permute(0, 2, 1),
),
dim=-1,
)
local_features = self.projector(local_features)
global_features_1 = self.sam_model(image_ori)
global_features_2 = self.vision_model(image_ori, global_features_1)
global_features = torch.cat(
(
global_features_2[:, 1:],
global_features_1.flatten(2).permute(0, 2, 1),
),
dim=-1,
)
global_features = self.projector(global_features)
_, hw, n_dim = global_features.shape
h = w = int(hw**0.5)
_2, hw2, n_dim2 = local_features.shape
h2 = w2 = int(hw2**0.5)
width_crop_num, height_crop_num = int(crop_shape[0]), int(
crop_shape[1]
)
global_features = global_features.view(h, w, n_dim)
global_features = torch.cat(
[
global_features,
self.image_newline[None, None, :].expand(h, 1, n_dim),
],
dim=1,
)
global_features = global_features.view(-1, n_dim)
local_features = (
local_features.view(
height_crop_num, width_crop_num, h2, w2, n_dim2
)
.permute(0, 2, 1, 3, 4)
.reshape(height_crop_num * h2, width_crop_num * w2, n_dim2)
)
local_features = torch.cat(
[
local_features,
self.image_newline[None, None, :].expand(
height_crop_num * h2, 1, n_dim2
),
],
dim=1,
)
local_features = local_features.view(-1, n_dim2)
use_local_crops = (
has_local_crops[jdx]
if has_local_crops is not None
else torch.sum(patches).item() != 0
)
global_features = self._encode_ocr2_features(image_ori)
if use_local_crops:
local_features = self._encode_ocr2_features(patches)
global_local_features = torch.cat(
[local_features, global_features, self.view_seperator[None, :]],
dim=0,
)
else:
global_features_1 = self.sam_model(image_ori)
global_features_2 = self.vision_model(image_ori, global_features_1)
global_features = torch.cat(
(
global_features_2[:, 1:],
global_features_1.flatten(2).permute(0, 2, 1),
),
dim=-1,
)
global_features = self.projector(global_features)
_, hw, n_dim = global_features.shape
h = w = int(hw**0.5)
global_features = global_features.view(h, w, n_dim)
global_features = torch.cat(
[
global_features,
self.image_newline[None, None, :].expand(h, 1, n_dim),
],
dim=1,
)
global_features = global_features.view(-1, n_dim)
global_local_features = torch.cat(
[global_features, self.view_seperator[None, :]], dim=0
)
@@ -1361,8 +1663,14 @@ class DeepseekOCRForCausalLM(nn.Module):
return images_in_this_batch
def _process_image_input(self, mm_items: List[MultimodalDataItem]) -> torch.Tensor:
target_dtype = (
next(self.sam_model.parameters()).dtype
if self.is_ocr2
else self.vision_model.dtype
)
has_local_crops = self._collect_mm_flag(mm_items, "has_local_crops")
pixel_values = torch.stack([item.feature for item in mm_items], dim=0).type(
self.vision_model.dtype
target_dtype
)
images_crop = (
@@ -1383,10 +1691,9 @@ class DeepseekOCRForCausalLM(nn.Module):
pixel_values=pixel_values,
images_crop=images_crop,
images_spatial_crop=images_spatial_crop,
has_local_crops=has_local_crops,
)
vision_features = torch.cat(vision_feature_lists, dim=0).type(
self.vision_model.dtype
)
vision_features = torch.cat(vision_feature_lists, dim=0).type(target_dtype)
return vision_features
@@ -1458,6 +1765,7 @@ class DeepseekOCRForCausalLM(nn.Module):
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
is_qwen2_weight = "qwen2_model." in name
if name == "lm_head.weight":
name = "model.lm_head.weight"
elif name.startswith("model."):
@@ -1465,6 +1773,7 @@ class DeepseekOCRForCausalLM(nn.Module):
"image_newline" in name
or ".projector" in name
or "vision_model" in name
or "qwen2_model" in name
or "sam_model" in name
or "view_seperator" in name
):
@@ -1472,11 +1781,32 @@ class DeepseekOCRForCausalLM(nn.Module):
elif not (
".projector" in name
or "vision_model" in name
or "qwen2_model" in name
or "sam_model" in name
or "image_newline" in name
):
name = name.replace("model.", "model.model.")
if is_qwen2_weight:
target_name = name
if target_name not in params_dict:
if ".model.model." in target_name:
alt_name = target_name.replace(".model.model.", ".model.")
else:
alt_name = target_name.replace(".model.", ".model.model.", 1)
if alt_name in params_dict:
target_name = alt_name
if target_name.endswith(".bias") and target_name not in params_dict:
continue
if target_name in params_dict:
param = params_dict[target_name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
loaded_params.add(target_name)
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue

View File

@@ -12,6 +12,14 @@ class DeepseekOCRProcessor(BaseMultimodalProcessor):
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
)
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
self.mm_tokens = MultimodalSpecialTokens(
image_token="<image>", image_token_id=self._processor.image_token_id

View File

@@ -230,11 +230,13 @@ def _load_mistral_large_3_for_causal_LM(
def _is_deepseek_ocr_model(config: PretrainedConfig) -> bool:
# TODO: Remove this workaround related when AutoConfig correctly identifies deepseek-ocr.
# Hugging Face's AutoConfig currently misidentifies it as deepseekvl2.
return (
getattr(config, "auto_map", None) is not None
and config.auto_map.get("AutoModel")
== "modeling_deepseekocr.DeepseekOCRForCausalLM"
)
auto_map = getattr(config, "auto_map", None) or {}
return auto_map.get("AutoModel") == "modeling_deepseekocr.DeepseekOCRForCausalLM"
def _is_deepseek_ocr2_model(config: PretrainedConfig) -> bool:
auto_map = getattr(config, "auto_map", None) or {}
return auto_map.get("AutoModel") == "modeling_deepseekocr2.DeepseekOCR2ForCausalLM"
def _override_deepseek_ocr_v_head_dim(config: DeepseekVLV2Config) -> None:
@@ -248,6 +250,30 @@ def _override_deepseek_ocr_v_head_dim(config: DeepseekVLV2Config) -> None:
)
def _override_v_head_dim_if_zero(config: PretrainedConfig, patch: int = 128) -> None:
text_config = getattr(config, "text_config", None)
language_config = getattr(config, "language_config", None)
target = text_config or language_config
if target is None:
return
if getattr(target, "v_head_dim", None) == 0:
setattr(target, "v_head_dim", patch)
logger.warning(
f"Overriding v_head_dim from 0 to {patch} to avoid potential issues."
)
def _ensure_llama_flash_attention2_compat() -> None:
"""Ensure LlamaFlashAttention2 symbol exists for remote code compatibility."""
try:
from transformers.models.llama import modeling_llama
except Exception:
return
if not hasattr(modeling_llama, "LlamaFlashAttention2"):
if hasattr(modeling_llama, "LlamaAttention"):
modeling_llama.LlamaFlashAttention2 = modeling_llama.LlamaAttention
@lru_cache_frozenset(maxsize=32)
def get_config(
model: str,
@@ -274,6 +300,7 @@ def get_config(
model, trust_remote_code=trust_remote_code, revision=revision, **kwargs
)
else:
_ensure_llama_flash_attention2_compat()
try:
config = AutoConfig.from_pretrained(
model, trust_remote_code=trust_remote_code, revision=revision, **kwargs
@@ -308,20 +335,38 @@ def get_config(
text_config = get_hf_text_config(config=config)
if isinstance(model, str) and text_config is not None:
for key, val in text_config.__dict__.items():
if not hasattr(config, key) and getattr(text_config, key, None) is not None:
items = (
text_config.items()
if hasattr(text_config, "items")
else vars(text_config).items()
)
for key, val in items:
if not hasattr(config, key) and val is not None:
setattr(config, key, val)
if config.model_type in _CONFIG_REGISTRY:
if _is_deepseek_ocr2_model(config):
_override_v_head_dim_if_zero(config)
# Temporary hack for load deepseek-ocr2
config.model_type = "deepseek-ocr"
config.update({"architectures": ["DeepseekOCRForCausalLM"]})
config = DeepseekVLV2Config.from_pretrained(model, revision=revision)
_override_v_head_dim_if_zero(config)
config.update({"architectures": ["DeepseekOCRForCausalLM"]})
setattr(config, "_name_or_path", model)
elif config.model_type in _CONFIG_REGISTRY:
model_type = config.model_type
if model_type == "deepseek_vl_v2":
if _is_deepseek_ocr_model(config):
if _is_deepseek_ocr_model(config) or _is_deepseek_ocr2_model(config):
model_type = "deepseek-ocr"
config_class = _CONFIG_REGISTRY[model_type]
config = config_class.from_pretrained(model, revision=revision)
if _is_deepseek_ocr_model(config):
_override_deepseek_ocr_v_head_dim(config)
config.update({"architectures": ["DeepseekOCRForCausalLM"]})
elif _is_deepseek_ocr2_model(config):
_override_v_head_dim_if_zero(config)
config.update({"architectures": ["DeepseekOCRForCausalLM"]})
# NOTE(HandH1998): Qwen2VL requires `_name_or_path` attribute in `config`.
setattr(config, "_name_or_path", model)
@@ -536,6 +581,7 @@ def get_processor(
**kwargs,
)
else:
_ensure_llama_flash_attention2_compat()
config = AutoConfig.from_pretrained(
tokenizer_name,
trust_remote_code=trust_remote_code,
@@ -545,6 +591,12 @@ def get_processor(
if _is_deepseek_ocr_model(config):
# Temporary hack for load deepseek-ocr
config.model_type = "deepseek-ocr"
config.update({"architectures": ["DeepseekOCRForCausalLM"]})
elif _is_deepseek_ocr2_model(config):
# Temporary hack for load deepseek-ocr2
config.model_type = "deepseek-ocr"
config.update({"architectures": ["DeepseekOCRForCausalLM"]})
_override_v_head_dim_if_zero(config)
# fix: for Qwen2-VL and Sarashina2Vision models, inject default 'size' if not provided.
if config.model_type in {"qwen2_vl", "sarashina2_vision"}: