from dataclasses import dataclass from typing import List, Optional, Tuple, Union import numpy as np import torch import torch.nn as nn from PIL import Image, ImageDraw, ImageFont, ImageOps from torch.nn import CrossEntropyLoss from tqdm import tqdm from transformers import LogitsProcessorList, TextStreamer from transformers.modeling_outputs import BaseModelOutputWithPast, MoeCausalLMOutputWithPast from transformers.utils import logging from .configuration_deepseek_v2 import DeepseekV2Config from .deepencoder import MlpProjector, _AttrDict, build_clip_l, build_sam_vit_b from .modeling_deepseekv2 import ( DeepseekV2ForCausalLM, DeepseekV2Model, _build_llama_rotary_embedding, _prepare_inputs_for_generation, ) logger = logging.get_logger(__name__) torch_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 @dataclass class CausalLMOutputWithPastForMTP(MoeCausalLMOutputWithPast): last_hidden_state: Optional[torch.Tensor] = None # extend the output for MTP and MoE models inputs_embeds: Optional[torch.Tensor] = None position_ids: Optional[torch.Tensor] = None mtp_hidden_states: Optional[list[torch.Tensor]] = None class NoEOSTextStreamer(TextStreamer): def on_finalized_text(self, text: str, stream_end: bool = False): eos_text = self.tokenizer.decode([self.tokenizer.eos_token_id], skip_special_tokens=False) text = text.replace(eos_text, "\n") print(text, flush=True, end="") # Match Unlimited-OCR generate() defaults. _DEFAULT_NGRAM_SIZE = 35 _DEFAULT_NGRAM_WINDOW = 1024 _DEFAULT_NGRAM_WHITELIST = frozenset({128821, 128822}) # , class SlidingWindowNoRepeatNgramProcessor: """Ban tokens that would complete an n-gram already seen in the last ``window`` tokens.""" def __init__(self, ngram_size, window, whitelist_token_ids=None): self.ngram_size = ngram_size self.window = window self.whitelist = set(whitelist_token_ids) if whitelist_token_ids else set() def __call__(self, input_ids, scores): for batch_idx in range(input_ids.shape[0]): sequence = input_ids[batch_idx].tolist() if len(sequence) < self.ngram_size: continue search_start = max(0, len(sequence) - self.window) search_end = len(sequence) - self.ngram_size + 1 if search_end <= search_start: continue if self.ngram_size > 1: current_prefix = tuple(sequence[-(self.ngram_size - 1) :]) else: current_prefix = tuple() banned = set() for idx in range(search_start, search_end): ngram = sequence[idx : idx + self.ngram_size] if self.ngram_size == 1 or tuple(ngram[:-1]) == current_prefix: banned.add(ngram[-1]) banned.difference_update(self.whitelist) for token_id in banned: scores[batch_idx, token_id] = float("-inf") return scores class DeepseekOCRConfig(DeepseekV2Config): model_type = "deepseek_vl_v2" def get_text_config(self, *args, **kwargs): # Decoder fields are flat on this object. Nested language_config is a # dict dump; vLLM rejects that as text_config (no num_attention_heads). return self class DeepseekOCRModel(DeepseekV2Model): config_class = DeepseekOCRConfig def __init__(self, config: DeepseekV2Config): super(DeepseekOCRModel, self).__init__(config) self.sam_model = build_sam_vit_b() self.vision_model = build_clip_l() # self.conv_2 = nn.Conv2d(in_channels=1024, out_channels=2048, kernel_size=2, stride=2) n_embed = 1280 self.projector = MlpProjector(_AttrDict(projector_type="linear", input_dim=2048, n_embed=n_embed)) embed_std = 1 / torch.sqrt(torch.tensor(n_embed, dtype=torch.float32)) self.image_newline = nn.Parameter(torch.randn(n_embed) * embed_std) self.view_seperator = nn.Parameter(torch.randn(n_embed) * embed_std) self.rotary_emb = _build_llama_rotary_embedding(config) def sam_vision_fwd(self, img_tensor): # Processor images stay float32; SAM/CLIP follow the loaded model dtype. img_tensor = img_tensor.to(dtype=next(self.sam_model.parameters()).dtype) sam_e = self.sam_model(img_tensor) vision_e = self.vision_model(img_tensor, sam_e) sam_e = sam_e.flatten(2).permute(0, 2, 1) vision_e = vision_e[:, 1:] concat_e = torch.cat((vision_e, sam_e), dim=-1) return self.projector(concat_e) def _sam_vision_fwd_local_global(self, patches: torch.Tensor, image_ori: torch.Tensor): """Run SAM+CLIP+projector on local crops and global images. When spatial sizes match (typical gundam 1024×1024), batch into one forward so SAM/CLIP kernels see a larger batch and avoid a second full tower pass. """ n_local = patches.shape[0] if patches.shape[-2:] == image_ori.shape[-2:]: fused = self.sam_vision_fwd(torch.cat([patches, image_ori], dim=0)) return fused[:n_local], fused[n_local:] return self.sam_vision_fwd(patches), self.sam_vision_fwd(image_ori) def compute_inputs_embeds( self, input_ids: torch.LongTensor = None, images: Optional[torch.FloatTensor] = None, images_seq_mask: Optional[torch.FloatTensor] = None, images_spatial_crop: Optional[torch.FloatTensor] = None, inputs_embeds: Optional[torch.FloatTensor] = None, ) -> torch.Tensor: """Compute ``inputs_embeds`` with vision features injected (no LM forward). Extracted from :meth:`forward` so callers with pre-computed hidden states (e.g. from vLLM) can get ``inputs_embeds`` for the MTP head without running the full DeepseekV2 language model. """ if inputs_embeds is None: inputs_embeds = self.get_input_embeddings()(input_ids) inputs_embeds = inputs_embeds.clone() B, seq_len, n_dim = inputs_embeds.shape if images is not None and (seq_len != 1 or self.training) and torch.sum(images[0][1]).item() != 0: patches, image_ori = zip(*images) patches = torch.cat(patches, dim=0) image_ori = torch.cat(image_ori, dim=0) if torch.sum(patches).item() != 0: local_features, global_features = self._sam_vision_fwd_local_global(patches, image_ori) img_B, hw, n_dim = global_features.shape h = w = int(hw**0.5) _, hw2, n_dim2 = local_features.shape h2 = w2 = int(hw2**0.5) global_features = global_features.view(img_B, h, w, n_dim) new_line_img_tkn = self.image_newline[None, None, :].expand(img_B, h, 1, n_dim) global_features = torch.cat([global_features, new_line_img_tkn], dim=2) global_features_list = [] from_i = 0 for i, crop_shape in enumerate(images_spatial_crop): width_crop_num, height_crop_num = crop_shape[0], crop_shape[1] local_features_i = local_features.new_empty(0, n_dim2) has_patches = width_crop_num > 1 or height_crop_num > 1 if has_patches: to_i = from_i + width_crop_num * height_crop_num local_features_i = ( local_features[from_i:to_i] .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_i = torch.cat( [ local_features_i, self.image_newline[None, None, :].expand(height_crop_num * h2, 1, n_dim2), ], dim=1, ) local_features_i = local_features_i.view(-1, n_dim2) from_i = to_i global_features_i = global_features[i].view(-1, n_dim) visual_sep_token = self.view_seperator[None, :] global_features_i = torch.cat([local_features_i, global_features_i, visual_sep_token], dim=0) global_features_list.append(global_features_i) global_local_features = torch.cat(global_features_list, dim=0) else: global_features = self.sam_vision_fwd(image_ori) img_B, hw, n_dim = global_features.shape h = w = int(hw**0.5) global_features = global_features.view(img_B, h, w, n_dim) new_line_img_tkn = self.image_newline[None, None, :].expand(img_B, h, 1, n_dim) global_features = torch.cat([global_features, new_line_img_tkn], dim=2) global_features = global_features.view(img_B, -1, n_dim) visual_sep_token = self.view_seperator[None, None, :].expand(img_B, -1, -1) global_local_features = torch.cat([global_features, visual_sep_token], dim=1) global_local_features = global_local_features.view(-1, n_dim) inputs_flat = inputs_embeds.view(B * seq_len, n_dim) mask_flat = images_seq_mask.view(B * seq_len) images_flat = global_local_features.view(-1, n_dim) n_mask = int(mask_flat.sum().item()) n_visual = images_flat.shape[0] if n_mask != n_visual: raise RuntimeError( f"Visual token mismatch: encoder produced {n_visual} tokens, " f"mask has {n_mask} positions (diff={n_visual - n_mask}). " f"B={B}, seq_len={seq_len}, " f"images_spatial_crop={images_spatial_crop.tolist()}" ) inputs_flat[mask_flat] = images_flat.to(inputs_flat.dtype) inputs_embeds = inputs_flat.view(B, seq_len, n_dim) return inputs_embeds def forward( self, input_ids: torch.LongTensor = None, attention_mask: Optional[torch.Tensor] = None, position_ids: Optional[torch.LongTensor] = None, past_key_values: Optional[List[torch.FloatTensor]] = None, inputs_embeds: Optional[torch.FloatTensor] = None, use_cache: Optional[bool] = None, output_attentions: Optional[bool] = None, output_hidden_states: Optional[bool] = None, output_router_logits: Optional[bool] = None, images: Optional[torch.FloatTensor] = None, images_seq_mask: Optional[torch.FloatTensor] = None, images_spatial_crop: Optional[torch.FloatTensor] = None, cache_position: Optional[torch.LongTensor] = None, return_dict: Optional[bool] = None, **kwargs, ) -> Union[Tuple, BaseModelOutputWithPast]: inputs_embeds = self.compute_inputs_embeds( input_ids=input_ids, images=images, images_seq_mask=images_seq_mask, images_spatial_crop=images_spatial_crop, inputs_embeds=inputs_embeds, ) return super(DeepseekOCRModel, self).forward( input_ids=None, attention_mask=attention_mask, past_key_values=past_key_values, inputs_embeds=inputs_embeds, use_cache=use_cache, position_ids=position_ids, output_attentions=output_attentions, output_hidden_states=output_hidden_states, output_router_logits=output_router_logits, return_dict=return_dict, cache_position=cache_position, ) class DeepseekOCRForCausalLM(DeepseekV2ForCausalLM): config_class = DeepseekOCRConfig model_class = DeepseekOCRModel # FastMTP draft tensors ship in the same checkpoint for vLLM. This class is # decoder-only — generate() has no speculative path — so ignore them on load. _keys_to_ignore_on_load_unexpected = [ r"(^|\.)mtp_module\.", r"(^|\.)mtp_embed_tokens", ] def __init__(self, config): super(DeepseekV2ForCausalLM, self).__init__(config) self.model = self.model_class(config) self.vocab_size = config.vocab_size self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) # Initialize weights and apply final processing self.post_init() @classmethod def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs): loaded = super().from_pretrained(pretrained_model_name_or_path, *model_args, **kwargs) model = loaded[0] if isinstance(loaded, tuple) else loaded if getattr(model.config, "mtp_num_heads", 0): logger.warning( "FastMTP draft weights (mtp_module.*, mtp_embed_tokens.*) were not loaded. " "Transformers generate() does not support MTP speculative decoding; " "only the 3B MoE decoder runs. Use vLLM with deepseek_ocr_mtp.register() " "for FastMTP." ) return loaded def get_model(self): return self.model def set_sam_gradient_checkpointing(self, mode: str = "off") -> None: """SAM GC is independent of LLM ``gradient_checkpointing``. Prefer FlexAttention (always on when available) for VRAM; use SAM GC only if still OOM. ``global`` checkpoints the 4 global-attn blocks only. """ sam = getattr(self.model, "sam_model", None) if sam is None: return if hasattr(sam, "set_gradient_checkpointing"): sam.set_gradient_checkpointing(mode) elif hasattr(sam, "gradient_checkpointing"): sam.gradient_checkpointing = mode not in (None, "off", False) def gradient_checkpointing_enable(self, gradient_checkpointing_kwargs=None): """Enable GC on the LLM decoder only — does not touch SAM (too slow for step time).""" super().gradient_checkpointing_enable(gradient_checkpointing_kwargs=gradient_checkpointing_kwargs) def gradient_checkpointing_disable(self): super().gradient_checkpointing_disable() self.set_sam_gradient_checkpointing("off") def forward( self, input_ids: torch.LongTensor = None, attention_mask: Optional[torch.Tensor] = None, position_ids: Optional[torch.LongTensor] = None, past_key_values: Optional[List[torch.FloatTensor]] = None, inputs_embeds: Optional[torch.FloatTensor] = None, labels: Optional[torch.LongTensor] = None, use_cache: Optional[bool] = None, output_attentions: Optional[bool] = None, output_hidden_states: Optional[bool] = None, output_router_logits: Optional[bool] = None, images: Optional[torch.FloatTensor] = None, images_seq_mask: Optional[torch.FloatTensor] = None, images_spatial_crop: Optional[torch.FloatTensor] = None, logits_to_keep: Union[int, torch.Tensor] = 0, skip_logits: Optional[bool] = None, cache_position: Optional[torch.LongTensor] = None, return_dict: Optional[bool] = None, **kwargs, ) -> Union[Tuple, CausalLMOutputWithPastForMTP]: output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions output_hidden_states = ( output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states ) output_router_logits = ( output_router_logits if output_router_logits is not None else self.config.output_router_logits ) return_dict = return_dict if return_dict is not None else self.config.use_return_dict # When True, skip the BT x V lm_head matmul so the trainer can run # LigerFusedLinearCrossEntropyLoss on ``last_hidden_state`` instead. # Default keeps the previous behaviour (always materialise logits). if skip_logits is None: skip_logits = False outputs = self.model( input_ids=input_ids, past_key_values=past_key_values, attention_mask=attention_mask, position_ids=position_ids, inputs_embeds=inputs_embeds, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, output_router_logits=output_router_logits, images=images, images_seq_mask=images_seq_mask, images_spatial_crop=images_spatial_crop, cache_position=cache_position, return_dict=return_dict, ) hidden_states = outputs[0] loss = None if skip_logits: # Trainer owns CE via fused linear; do not materialise logits here. # Labels (if any) are ignored — the trainer always pops them before # calling forward and computes the loss itself. logits = None else: # Only compute necessary logits, and do not upcast them to float if we are not computing the loss slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep logits = self.lm_head(hidden_states[:, slice_indices, :]) # Cast logits to fp32 for numerical stability during training. # During inference, the logits are cast back to the original dtype (e.g., bfloat16 or float16) to save memory and speed up generation. if self.training: logits = logits.float() if labels is not None: # Shift so that tokens < n predict n shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = CrossEntropyLoss() shift_logits = shift_logits.view(-1, self.config.vocab_size) shift_labels = shift_labels.view(-1) # Enable model parallelism shift_labels = shift_labels.to(shift_logits.device) loss = loss_fct(shift_logits, shift_labels) if not return_dict: output = (logits,) + outputs[1:] return (loss,) + output if loss is not None else output return CausalLMOutputWithPastForMTP( loss=loss, aux_loss=None, logits=logits, last_hidden_state=hidden_states, past_key_values=outputs.past_key_values, hidden_states=outputs.hidden_states, attentions=outputs.attentions, router_logits=outputs.router_logits, inputs_embeds=getattr(outputs, "inputs_embeds", None), position_ids=getattr(outputs, "position_ids", None), ) def prepare_inputs_for_generation(self, input_ids, **kwargs): return _prepare_inputs_for_generation(self, input_ids, **kwargs) def generate(self, *args, **kwargs): ngram_size = kwargs.pop("no_repeat_ngram_size", _DEFAULT_NGRAM_SIZE) window_size = kwargs.pop("ngram_window", _DEFAULT_NGRAM_WINDOW) whitelist_token_ids = kwargs.pop("whitelist_token_ids", _DEFAULT_NGRAM_WHITELIST) processors = kwargs.get("logits_processor") if processors is None: processors = LogitsProcessorList() elif not isinstance(processors, (list, tuple)): processors = LogitsProcessorList([processors]) else: processors = LogitsProcessorList(processors) already = any(isinstance(p, SlidingWindowNoRepeatNgramProcessor) for p in processors) if ngram_size and window_size and not already: processors.append(SlidingWindowNoRepeatNgramProcessor(ngram_size, window_size, whitelist_token_ids)) if processors: kwargs["logits_processor"] = processors return super().generate(*args, **kwargs) def disable_torch_init(self): """ Disable the redundant torch default initialization to accelerate model creation. """ import torch setattr(torch.nn.Linear, "reset_parameters", lambda self: None) setattr(torch.nn.LayerNorm, "reset_parameters", lambda self: None)