94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
439 lines
17 KiB
Python
439 lines
17 KiB
Python
"""Standalone UNLIMITED-OCR model (SAM + CLIP vision encoders, Deepseek backbone)."""
|
|
|
|
import logging
|
|
from typing import Iterable, List, Optional, Set, Tuple, TypeAlias, Union
|
|
|
|
import torch
|
|
from torch import Tensor, nn
|
|
|
|
from sglang.srt.configs.unlimited_ocr import UnlimitedVLConfig
|
|
from sglang.srt.layers.quantization import QuantizationConfig
|
|
from sglang.srt.managers.mm_utils import (
|
|
MultiModalityDataPaddingPatternMultimodalTokens,
|
|
general_mm_embed_routine,
|
|
)
|
|
from sglang.srt.managers.schedule_batch import MultimodalDataItem, MultimodalInputs
|
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
|
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
|
from sglang.srt.models.deepseek import DeepseekForCausalLM
|
|
from sglang.srt.models.deepseek_ocr import (
|
|
MlpProjector,
|
|
build_clip_l,
|
|
build_sam_vit_b,
|
|
merge_multimodal_embeddings,
|
|
)
|
|
from sglang.srt.models.transformers import maybe_prefix
|
|
from sglang.srt.utils import cpu_has_amx_support, is_cpu
|
|
|
|
_is_cpu_amx_available = cpu_has_amx_support()
|
|
_is_cpu = is_cpu()
|
|
|
|
NestedTensors: TypeAlias = Union[
|
|
list["NestedTensors"],
|
|
list["torch.Tensor"],
|
|
"torch.Tensor",
|
|
tuple["torch.Tensor", ...],
|
|
]
|
|
|
|
MultiModalEmbeddings: TypeAlias = list[Tensor] | Tensor | tuple[Tensor, ...]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class UnlimitedOCRForCausalLM(nn.Module):
|
|
"""Standalone UNLIMITED-OCR model (SAM + CLIP ViT) with prefill-aware SWA."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
config: UnlimitedVLConfig,
|
|
quant_config: Optional[QuantizationConfig] = None,
|
|
prefix: str = "",
|
|
):
|
|
"""Initialize UnlimitedOCRForCausalLM with vision encoders, projector, and LM."""
|
|
super().__init__()
|
|
|
|
self.config = config
|
|
self.vision_config = config.vision_config
|
|
self.projector_config = config.projector_config
|
|
self.text_config = config.text_config
|
|
|
|
n_embed = getattr(self.projector_config, "n_embed", 1280)
|
|
|
|
self.tile_tag = config.tile_tag
|
|
self.global_view_pos = config.global_view_pos
|
|
|
|
embed_std = 1 / torch.sqrt(torch.tensor(n_embed, dtype=torch.float32))
|
|
if self.tile_tag == "2D":
|
|
self.view_seperator = nn.Parameter(torch.randn(n_embed) * embed_std)
|
|
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}"
|
|
)
|
|
|
|
self.model = DeepseekForCausalLM(
|
|
config=config.text_config,
|
|
quant_config=quant_config,
|
|
prefix=maybe_prefix(prefix, "language"),
|
|
)
|
|
|
|
self.sam_model = build_sam_vit_b()
|
|
self.vision_model = build_clip_l()
|
|
|
|
self.projector = MlpProjector(
|
|
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,
|
|
)
|
|
|
|
self.image_token_id = None
|
|
|
|
def get_attention_sliding_window_size(self) -> Optional[int]:
|
|
"""Return the sliding window size from the model config, or None."""
|
|
return getattr(self.config, "sliding_window_size", None)
|
|
|
|
def is_prefill_aware_swa(self) -> bool:
|
|
"""Prefill tokens are always retained in KV cache during decode."""
|
|
return True
|
|
|
|
def _encode_ocr1_features(self, images: torch.Tensor) -> torch.Tensor:
|
|
"""Encode images through SAM and CLIP encoders, then project features."""
|
|
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:
|
|
"""Reshape global features into a flat sequence with newline tokens."""
|
|
_, 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:
|
|
"""Reshape local crop features into a flat sequence with newline tokens."""
|
|
_, 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)
|
|
|
|
@staticmethod
|
|
def _collect_mm_flag(
|
|
items: List[MultimodalDataItem], flag_name: str
|
|
) -> Optional[List[bool]]:
|
|
"""Collect a boolean multimodal flag from all data items."""
|
|
values = []
|
|
for item in items:
|
|
value = getattr(item, flag_name, None)
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, list):
|
|
values.extend(value)
|
|
else:
|
|
values.append(bool(value))
|
|
return values
|
|
|
|
def _parse_and_validate_image_input(self, **kwargs: object):
|
|
"""Parse and validate pixel values, spatial crops, and image crops."""
|
|
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:
|
|
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:
|
|
if not isinstance(pixel_values, (torch.Tensor, list)):
|
|
raise ValueError(
|
|
"Incorrect type of pixel values. " f"Got type: {type(pixel_values)}"
|
|
)
|
|
if not isinstance(images_spatial_crop, (torch.Tensor, list)):
|
|
raise ValueError(
|
|
"Incorrect type of image sizes. "
|
|
f"Got type: {type(images_spatial_crop)}"
|
|
)
|
|
if not isinstance(images_crop, (torch.Tensor, list)):
|
|
raise ValueError(
|
|
"Incorrect type of image crop. " f"Got type: {type(images_crop)}"
|
|
)
|
|
return [pixel_values, images_crop, images_spatial_crop]
|
|
|
|
raise AssertionError("This line should be unreachable.")
|
|
|
|
def _pixel_values_to_embedding(
|
|
self,
|
|
pixel_values: torch.Tensor,
|
|
images_crop: torch.Tensor,
|
|
images_spatial_crop: torch.Tensor,
|
|
has_local_crops: Optional[List[bool]] = None,
|
|
) -> NestedTensors:
|
|
"""Encode pixel values into per-image embedding sequences."""
|
|
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]
|
|
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
|
|
|
|
def _process_image_input(self, mm_items: List[MultimodalDataItem]) -> torch.Tensor:
|
|
"""Process multimodal data items into concatenated vision features."""
|
|
target_dtype = 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(
|
|
target_dtype
|
|
)
|
|
|
|
images_crop = (
|
|
torch.stack([item.images_crop for item in mm_items], dim=0)
|
|
.type(target_dtype)
|
|
.to(device=pixel_values.device)
|
|
)
|
|
images_spatial_crop = (
|
|
torch.cat([item.images_spatial_crop for item in mm_items], dim=0)
|
|
.type(torch.long)
|
|
.to(device=pixel_values.device)
|
|
)
|
|
pixel_values = pixel_values.view(
|
|
pixel_values.shape[0] * pixel_values.shape[1], 1, *pixel_values.shape[2:]
|
|
)
|
|
images_crop = images_crop.view(
|
|
images_crop.shape[0] * images_crop.shape[1], 1, *images_crop.shape[2:]
|
|
)
|
|
images_spatial_crop = images_spatial_crop.view(
|
|
images_spatial_crop.shape[0] * images_spatial_crop.shape[1],
|
|
1,
|
|
*images_spatial_crop.shape[2:],
|
|
)
|
|
|
|
assert images_crop.dim() == 6
|
|
assert images_spatial_crop.dim() == 3
|
|
|
|
vision_feature_lists = self._pixel_values_to_embedding(
|
|
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(target_dtype)
|
|
return vision_features
|
|
|
|
def get_language_model(self) -> torch.nn.Module:
|
|
"""Return the underlying language model."""
|
|
return self.model
|
|
|
|
def get_multimodal_embeddings(
|
|
self, **kwargs: object
|
|
) -> Optional[MultiModalEmbeddings]:
|
|
"""Compute multimodal embeddings from image inputs, if present."""
|
|
image_input = self._parse_and_validate_image_input(**kwargs)
|
|
if image_input is None:
|
|
return None
|
|
vision_embeddings = self._process_image_input(image_input)
|
|
return vision_embeddings
|
|
|
|
def get_input_embeddings(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
|
|
) -> torch.Tensor:
|
|
"""Get text embeddings and merge in multimodal embeddings if provided."""
|
|
inputs_embeds = self.model.get_input_embeddings(input_ids)
|
|
if multimodal_embeddings is not None:
|
|
inputs_embeds = merge_multimodal_embeddings(
|
|
input_ids, inputs_embeds, multimodal_embeddings, self.image_token_id
|
|
)
|
|
return inputs_embeds
|
|
|
|
def pad_input_ids(self, input_ids: List[int], mm_inputs: MultimodalInputs):
|
|
"""Pad input token IDs with multimodal placeholder tokens."""
|
|
pattern = MultiModalityDataPaddingPatternMultimodalTokens()
|
|
return pattern.pad_input_tokens(input_ids, mm_inputs)
|
|
|
|
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
|
|
"""Extract vision features from multimodal data items."""
|
|
vision_embeddings = self._process_image_input(items)
|
|
return vision_embeddings
|
|
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
forward_batch: ForwardBatch,
|
|
**kwargs: object,
|
|
):
|
|
"""Run the full multimodal forward pass (embed, encode, decode)."""
|
|
hidden_states = general_mm_embed_routine(
|
|
input_ids=input_ids,
|
|
forward_batch=forward_batch,
|
|
language_model=self.model,
|
|
multimodal_model=self,
|
|
positions=positions,
|
|
)
|
|
return hidden_states
|
|
|
|
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
|
|
"""Load and remap checkpoint weights into the model parameters."""
|
|
stacked_params_mapping = [
|
|
(".qkv_proj", ".q_proj", "q"),
|
|
(".qkv_proj", ".k_proj", "k"),
|
|
(".qkv_proj", ".v_proj", "v"),
|
|
(".gate_up_proj", ".gate_proj", 0),
|
|
(".gate_up_proj", ".up_proj", 1),
|
|
]
|
|
|
|
params_dict = dict(self.named_parameters())
|
|
loaded_params: Set[str] = set()
|
|
for name, loaded_weight in weights:
|
|
if "rotary_emb.inv_freq" in name:
|
|
continue
|
|
if name == "lm_head.weight":
|
|
name = "model.lm_head.weight"
|
|
elif name.startswith("model."):
|
|
if (
|
|
"image_newline" in name
|
|
or ".projector" in name
|
|
or "vision_model" in name
|
|
or "sam_model" in name
|
|
or "view_seperator" in name
|
|
):
|
|
name = name[len("model.") :]
|
|
elif not (
|
|
".projector" in name
|
|
or "vision_model" in name
|
|
or "sam_model" in name
|
|
or "image_newline" in name
|
|
):
|
|
name = name.replace("model.", "model.model.")
|
|
|
|
for param_name, weight_name, shard_id in stacked_params_mapping:
|
|
if weight_name not in name:
|
|
continue
|
|
name = name.replace(weight_name, param_name)
|
|
if name.endswith(".bias") and name not in params_dict:
|
|
continue
|
|
if (
|
|
"mlp.experts." in name or "mlp.shared_experts." in name
|
|
) and name not in params_dict:
|
|
continue
|
|
param = params_dict[name]
|
|
weight_loader = param.weight_loader
|
|
weight_loader(param, loaded_weight, shard_id)
|
|
break
|
|
else:
|
|
if name.endswith(".bias") and name not in params_dict:
|
|
continue
|
|
if (
|
|
"mlp.experts." in name or "mlp.shared_experts." in name
|
|
) and name not in params_dict:
|
|
continue
|
|
param = params_dict[name]
|
|
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
|
weight_loader(param, loaded_weight)
|
|
loaded_params.add(name)
|
|
unloaded_params = params_dict.keys() - loaded_params
|
|
if unloaded_params:
|
|
raise RuntimeError(
|
|
f"Some weights are not initialized from checkpoints: {unloaded_params}"
|
|
)
|
|
self.post_load_weights()
|
|
|
|
def post_load_weights(self):
|
|
"""Apply post-loading weight transformations (e.g., AMX repacking on CPU)."""
|
|
if _is_cpu and _is_cpu_amx_available:
|
|
from sglang.srt.layers.amx_utils import _amx_process_weight_after_loading
|
|
|
|
layer_ids = int(self.config.num_hidden_layers)
|
|
first_k_dense_replace_id = (
|
|
self.config.first_k_dense_replace
|
|
if hasattr(self.config, "first_k_dense_replace")
|
|
else -1
|
|
)
|
|
moe_layer_freq_id = (
|
|
self.config.moe_layer_freq
|
|
if hasattr(self.config, "moe_layer_freq")
|
|
else 1
|
|
)
|
|
for layer_id in range(0, layer_ids):
|
|
if (
|
|
layer_id >= first_k_dense_replace_id
|
|
and layer_id % moe_layer_freq_id == 0
|
|
):
|
|
if (
|
|
hasattr(self.model, "model")
|
|
and hasattr(self.model.model, "layers")
|
|
and hasattr(self.model.model.layers[layer_id], "mlp")
|
|
):
|
|
self_moe = self.model.model.layers[layer_id].mlp
|
|
if hasattr(self_moe, "w1") and hasattr(self_moe, "w2"):
|
|
_amx_process_weight_after_loading(self_moe, ["w1", "w2"])
|
|
|
|
|
|
EntryClass = [UnlimitedOCRForCausalLM]
|