[perf] overlap page preprocessing, pack the vit, enable prefill CUDA graph for paddle-ocr (#35318)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mick
2026-08-19 08:20:55 +08:00
committed by GitHub
co-authored by Claude Opus 5
parent 64e404263e
commit 77fc5c128e
13 changed files with 1278 additions and 265 deletions
@@ -1920,6 +1920,7 @@ multimodal_piecewise_cuda_graph_supported_model_archs = [
# capturing cleanly.
multimodal_breakable_cuda_graph_supported_model_archs = [
"InternS2MobiusForConditionalGeneration",
"PaddleOCRVLForConditionalGeneration",
"Qwen3_5ForConditionalGeneration",
"Qwen3_5MoeForConditionalGeneration",
"MuseGlimmerForConditionalGeneration",
+3
View File
@@ -267,6 +267,9 @@ class Ernie4Model(nn.Module):
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def get_input_embeddings(self) -> nn.Embedding:
return self.embed_tokens
@torch.no_grad()
def forward(
self,
+227 -263
View File
@@ -13,14 +13,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""PaddleOCR-VL: a NaViT-style SigLIP vision encoder on an ERNIE-4.5 backbone.
The vision tower runs on a *packed* layout: every image of a (possibly
cross-request) batch is concatenated into one ``[total_patches, dim]`` tensor and
the per-image boundaries live on the host as ``grid_thws`` plus ``cu_seqlens``.
Keeping the boundaries host-side is what lets the whole ViT forward run without a
single device-to-host synchronization, and it lets the shape-independent
projections run once for the batch instead of once per image.
"""
import itertools
from collections.abc import Iterable
from typing import List, Optional, Set, Tuple, Union
from typing import List, Optional, Set, Tuple
import numpy as np
import torch
import torch.nn as nn
from einops import rearrange
from transformers.activations import GELUActivation
from transformers.utils import torch_int
@@ -43,8 +51,80 @@ from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.models.ernie4 import Ernie4_5_ForCausalLM
from sglang.srt.utils import add_prefix, is_npu
_is_npu = is_npu()
# Patch counts (t, h, w) of one image, always materialized on the host.
GridTHW = Tuple[int, int, int]
def build_packed_2d_position_ids(
grid_thws: List[GridTHW], device: torch.device
) -> Tuple[torch.Tensor, int]:
"""Row/column patch indices of a packed batch, plus the rope table size.
Returns ``([total_patches, 2], max_grid_size)``. ``max_grid_size`` is derived
from the host grids rather than from a device-side ``max()``, so building the
rope table never synchronizes.
"""
split_hids = list()
split_wids = list()
for t, h, w in grid_thws:
frame_ids = torch.arange(h * w, device=device)
hids = frame_ids // w
wids = frame_ids - hids * w
if t > 1:
hids = hids.repeat(t)
wids = wids.repeat(t)
split_hids.append(hids)
split_wids.append(wids)
if len(grid_thws) == 1:
height_position_ids, width_position_ids = split_hids[0], split_wids[0]
else:
height_position_ids = torch.cat(split_hids, dim=0)
width_position_ids = torch.cat(split_wids, dim=0)
pids = torch.stack([height_position_ids, width_position_ids], dim=-1)
max_grid_size = max(max(h, w) for _, h, w in grid_thws)
return pids, max_grid_size
def merge_patch_neighbourhoods(
hidden_states: torch.Tensor,
grid_thws: List[GridTHW],
merge_kernel_size: Tuple[int, int],
) -> torch.Tensor:
"""Group each image's ``m1 x m2`` patch neighbourhoods into single tokens.
Takes the packed ``[total_patches, dim]`` batch and returns
``[total_patches / (m1 * m2), m1 * m2 * dim]``. Only this step depends on an
image's ``h``/``w``, which is why it is separated from the projections that
follow: those are row-wise and run once for the whole batch.
"""
m1, m2 = merge_kernel_size
dim = hidden_states.shape[-1]
merged = hidden_states.new_empty(hidden_states.shape[0] // (m1 * m2), m1 * m2 * dim)
in_offset = out_offset = 0
for t, h, w in grid_thws:
num_patches = t * h * w
num_merged = num_patches // (m1 * m2)
# Row-major patches (t, h, w) regroup as (t, h/m1, m1, w/m2, m2, d); the
# merged token concatenates the m1*m2 neighbours along `d`.
merged[out_offset : out_offset + num_merged].view(
t, h // m1, w // m2, m1, m2, dim
).copy_(
hidden_states[in_offset : in_offset + num_patches]
.view(t, h // m1, m1, w // m2, m2, dim)
.permute(0, 1, 3, 2, 4, 5)
)
in_offset += num_patches
out_offset += num_merged
return merged
class Projector(nn.Module):
"""Merge 2x2 patch neighbourhoods, then project into the language space."""
def __init__(
self,
@@ -73,40 +153,22 @@ class Projector(nn.Module):
def forward(
self,
image_features: torch.Tensor,
image_grid_thw: List[Tuple[int, int, int]],
grid_thws: List[GridTHW],
) -> torch.Tensor:
m1, m2 = self.merge_kernel_size
if isinstance(image_features, (list, tuple)):
processed_features = list()
for image_feature, image_grid in zip(image_features, image_grid_thw):
image_feature = self.pre_norm(image_feature)
t, h, w = image_grid
"""Project packed ViT features ``[total_patches, dim]`` for the batch.
image_feature = rearrange(
image_feature,
"(t h p1 w p2) d -> (t h w) (p1 p2 d)",
t=t,
h=h // m1,
p1=m1,
w=w // m2,
p2=m2,
)
hidden_states = self.linear_1(image_feature)
hidden_states = self.act(hidden_states)
hidden_states = self.linear_2(hidden_states)
processed_features.append(hidden_states)
return processed_features
dims = image_features.shape[:-1]
dim = image_features.shape[-1]
image_features = image_features.view(np.prod(dims), dim)
hidden_states = self.pre_norm(image_features).view(-1, self.hidden_size)
hidden_states = self.linear_1(hidden_states)
Only the 2x2 merge depends on an image's ``h``/``w``; the norm and both
projections are row-wise, so they run once over the packed batch. Each
image contributes a single strided copy into the merged buffer, so an
N-image batch costs N copies plus 3 kernels rather than 4N kernels.
"""
hidden_states = self.pre_norm(image_features)
merged = merge_patch_neighbourhoods(
hidden_states, grid_thws, self.merge_kernel_size
)
hidden_states = self.linear_1(merged)
hidden_states = self.act(hidden_states)
hidden_states = self.linear_2(hidden_states)
return hidden_states.view(*dims, -1)
return self.linear_2(hidden_states)
class SiglipVisionEmbeddings(nn.Module):
@@ -118,12 +180,16 @@ class SiglipVisionEmbeddings(nn.Module):
self.image_size = config.image_size
self.patch_size = config.patch_size
# kernel_size == stride and padding == 0, so this convolution is exactly
# an unfold plus a matmul. Taking that path avoids a cuDNN convolution
# launch over a [total_patches, 3, p, p] input on every ViT forward.
self.patch_embedding = Conv2dLayer(
in_channels=config.num_channels,
out_channels=self.embed_dim,
kernel_size=self.patch_size,
stride=self.patch_size,
padding="valid",
disable_linear=False,
)
self.num_patches = (self.image_size // self.patch_size) ** 2
@@ -139,44 +205,43 @@ class SiglipVisionEmbeddings(nn.Module):
persistent=False,
)
def interpolate_pos_encoding(
self,
embeddings: torch.Tensor,
height: int,
width: int,
is_after_patchify: bool = False,
) -> torch.Tensor:
def interpolate_pos_encoding(self, height: int, width: int) -> torch.Tensor:
"""Resample the square learned position grid onto a ``height x width`` grid."""
num_positions = self.position_embedding.weight.shape[0]
patch_pos_embed = self.position_embedding.weight.unsqueeze(0)
dim = embeddings.shape[-1]
if is_after_patchify:
new_height = height
new_width = width
else:
new_height = height // self.patch_size
new_width = width // self.patch_size
sqrt_num_positions = torch_int(num_positions**0.5)
patch_pos_embed = patch_pos_embed.reshape(
1, sqrt_num_positions, sqrt_num_positions, dim
1, sqrt_num_positions, sqrt_num_positions, self.embed_dim
)
patch_pos_embed = patch_pos_embed.permute(0, 3, 1, 2)
patch_pos_embed = nn.functional.interpolate(
patch_pos_embed,
size=(new_height, new_width),
size=(height, width),
mode="bilinear",
align_corners=False,
)
patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim)
return patch_pos_embed
patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(
1, -1, self.embed_dim
)
# Materialize contiguously. The permute leaves the channel dim strided,
# and this tensor is cached and broadcast-added to the packed activations
# on every forward, so a strided read would be paid over and over.
return patch_pos_embed.contiguous()
def fetch_position_embedding_lfu_cache(self, embeddings, h, w, max_cache: int = 20):
def fetch_position_embedding_lfu_cache(
self, h: int, w: int, max_cache: int = 20
) -> torch.Tensor:
"""Return the interpolated position grid for ``(h, w)``, LFU-cached.
The interpolation depends only on the grid, so document batches that
repeat a resolution reuse the tensor instead of re-running the bilinear
resample once per image per forward. The cache holds at most `max_cache`
grids of `h * w * hidden_size` each (~12 MiB at the checkpoint's default
1280-token page budget), evicting the least frequently used.
"""
grid = (h, w)
if grid in self.cache_position_embedding:
self.cache_position_count[grid] += 1
@@ -190,7 +255,7 @@ class SiglipVisionEmbeddings(nn.Module):
self.cache_position_count.pop(min_hit_grid)
self.cache_position_embedding.pop(min_hit_grid)
position_embedding = self.interpolate_pos_encoding(embeddings, h, w, True)
position_embedding = self.interpolate_pos_encoding(h, w)
self.cache_position_count[grid] = 1
self.cache_position_embedding[grid] = position_embedding
return position_embedding
@@ -198,61 +263,39 @@ class SiglipVisionEmbeddings(nn.Module):
def forward(
self,
pixel_values: torch.FloatTensor,
grid_thws: List[GridTHW],
position_ids: Optional[torch.Tensor] = None,
image_grid_thw: Optional[
List[
Union[
Tuple[int, int, int],
List[Tuple[int, int, int]],
]
]
] = None,
interpolate_pos_encoding=False,
) -> torch.Tensor:
if pixel_values.dim() == 4:
pixel_values = pixel_values.unsqueeze(0)
if pixel_values.dim() == 5:
if position_ids is None:
raise ValueError(
"position_ids cannot be None when pixel_values.dim() is 5."
)
(
batch_size,
squence_len,
channel,
height,
width,
) = pixel_values.shape
target_dtype = self.patch_embedding.weight.dtype
pixel_values = rearrange(pixel_values, "b l c h w -> (b l) c h w")
patch_embeds = self.patch_embedding(pixel_values.to(dtype=target_dtype))
embeddings = patch_embeds.flatten(-2).squeeze(-1)
if interpolate_pos_encoding and image_grid_thw is not None:
start = 0
tmp_embeddings = list()
for image_grid in image_grid_thw:
t, h, w = image_grid
end = start + t * h * w
image_embeddings = embeddings[start:end, :]
position_embedding = (
self.interpolate_pos_encoding(image_embeddings, h, w, True)
.squeeze(0)
.repeat(t, 1)
)
image_embeddings = image_embeddings + position_embedding
tmp_embeddings.append(image_embeddings)
start = end
embeddings = torch.concat(tmp_embeddings, dim=0).unsqueeze(0)
else:
embeddings = embeddings + self.packing_position_embedding(position_ids)
return embeddings
else:
# [batch, patches, c, ph, pw] -> [batch * patches, c, ph, pw]
pixel_values = pixel_values.flatten(0, 1)
if pixel_values.dim() != 4:
raise ValueError(
"Unsupported pixel_values dimension:"
f" {pixel_values.dim()}. Expected 4 or 5."
)
patch_embeds = self.patch_embedding(
pixel_values.to(dtype=self.patch_embedding.weight.dtype)
)
# Each patch convolves to a 1x1 map, so this is a reshape to [patches, dim].
embeddings = patch_embeds.flatten(-2).squeeze(-1)
if position_ids is None:
# Interpolated per-image position grids, added in place so the packed
# activation is never copied into a second buffer.
offset = 0
for t, h, w in grid_thws:
num_patches = t * h * w
embeddings[offset : offset + num_patches].view(
t, h * w, self.embed_dim
).add_(self.fetch_position_embedding_lfu_cache(h, w))
offset += num_patches
else:
embeddings += self.packing_position_embedding(position_ids)
return embeddings.unsqueeze(0)
class SigLIPRotaryEmbedding(nn.Module):
@@ -347,10 +390,10 @@ class SiglipEncoderLayer(nn.Module):
def forward(
self,
hidden_states: torch.Tensor,
cu_seqlens: Optional[List[torch.Tensor]] = None,
rope_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
forward_metadata: Optional[VisionAttentionMetadata] = None,
) -> Tuple[torch.FloatTensor]:
cu_seqlens: torch.Tensor,
rope_emb: Tuple[torch.Tensor, torch.Tensor],
forward_metadata: VisionAttentionMetadata,
) -> torch.Tensor:
residual = hidden_states
@@ -399,69 +442,38 @@ class SiglipEncoder(nn.Module):
)
self.rotary_pos_emb = SigLIPRotaryEmbedding(head_dim // 2)
@staticmethod
def flatten_list(image_grid_thw):
tmp_image_grid_thw = list()
for image_grid in image_grid_thw:
if isinstance(image_grid, list):
tmp_image_grid_thw.extend(image_grid)
else:
tmp_image_grid_thw.append(image_grid)
return tmp_image_grid_thw
def _build_rope_emb(
self, grid_thws: List[GridTHW], device: torch.device
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Build the packed 2D rope cos/sin table for the batch."""
pids, max_grid_size = build_packed_2d_position_ids(grid_thws, device)
rope_emb = self.rotary_pos_emb(max_grid_size)[pids].flatten(1)
rope_emb = rope_emb.repeat(1, 2)
return rope_emb.cos(), rope_emb.sin()
def forward(
self,
inputs_embeds,
cu_seqlens: Optional[List[torch.Tensor]] = None,
image_grid_thw: Optional[
List[
Union[
Tuple[int, int, int],
List[Tuple[int, int, int]],
]
]
] = None,
height_position_ids: Optional[torch.Tensor] = None,
width_position_ids: Optional[torch.Tensor] = None,
inputs_embeds: torch.Tensor,
cu_seqlens: torch.Tensor,
grid_thws: List[GridTHW],
max_seqlen: int,
) -> torch.Tensor:
device = inputs_embeds.device
hidden_states = inputs_embeds
flatten_image_grid_thw = self.flatten_list(image_grid_thw)
rope_emb = self._build_rope_emb(grid_thws, inputs_embeds.device)
if width_position_ids is None or height_position_ids is None:
split_hids = list()
split_wids = list()
for t, h, w in flatten_image_grid_thw:
image_pids = torch.arange(t * h * w, device=device) % (h * w)
sample_hids = image_pids // w
sample_wids = image_pids % w
split_hids.append(sample_hids)
split_wids.append(sample_wids)
width_position_ids = torch.concat(split_wids, dim=0)
height_position_ids = torch.concat(split_hids, dim=0)
pids = torch.stack(
[height_position_ids, width_position_ids],
dim=-1,
)
max_grid_size = pids.max() + 1
rope_emb_max_grid = self.rotary_pos_emb(max_grid_size)
rope_emb = rope_emb_max_grid[pids].flatten(1)
rope_emb = rope_emb.repeat(1, 2)
rope_emb = (rope_emb.cos(), rope_emb.sin())
# cu_seqlens must be on cpu because of npu_flash_attention_unpad operator restriction
if is_npu() and isinstance(cu_seqlens, torch.Tensor):
if _is_npu:
cu_seqlens = cu_seqlens.to("cpu")
attn_cu_seqlens = cu_seqlens
# `max_seqlen` comes from the host grids, so the metadata is built once
# for every layer without reading a device tensor back.
forward_metadata = prepare_vision_attention_metadata(
attn_cu_seqlens, device=hidden_states.device
cu_seqlens, device=inputs_embeds.device, max_seqlen=max_seqlen
)
hidden_states = inputs_embeds
hidden_states = inputs_embeds
for encoder_layer in self.layers:
hidden_states = encoder_layer(
hidden_states,
cu_seqlens=attn_cu_seqlens,
cu_seqlens=cu_seqlens,
rope_emb=rope_emb,
forward_metadata=forward_metadata,
)
@@ -490,52 +502,28 @@ class SiglipVisionTransformer(nn.Module):
def forward(
self,
pixel_values,
interpolate_pos_encoding: Optional[bool] = False,
pixel_values: torch.Tensor,
grid_thws: List[GridTHW],
cu_seqlens: torch.Tensor,
max_seqlen: int,
position_ids: Optional[torch.Tensor] = None,
height_position_ids: Optional[torch.Tensor] = None,
width_position_ids: Optional[torch.Tensor] = None,
cu_seqlens: Optional[List[torch.Tensor]] = None,
image_grid_thw: Optional[
List[
Union[
Tuple[int, int, int],
List[Tuple[int, int, int]],
]
]
] = None,
) -> list[torch.Tensor]:
) -> torch.Tensor:
hidden_states = self.embeddings(
pixel_values,
interpolate_pos_encoding=interpolate_pos_encoding,
grid_thws=grid_thws,
position_ids=position_ids,
image_grid_thw=image_grid_thw,
)
last_hidden_state = self.encoder(
hidden_states = self.encoder(
inputs_embeds=hidden_states,
cu_seqlens=cu_seqlens,
image_grid_thw=image_grid_thw,
height_position_ids=height_position_ids,
width_position_ids=width_position_ids,
grid_thws=grid_thws,
max_seqlen=max_seqlen,
)
last_hidden_state = self.post_layernorm(last_hidden_state)
sample_hidden_state = list()
if cu_seqlens is None:
raise ValueError(
"cu_seqlens cannot be None for "
"SiglipVisionTransformer output processing."
)
for i in range(cu_seqlens.shape[0] - 1):
start = cu_seqlens[i]
end = cu_seqlens[i + 1]
tensor = last_hidden_state[:, start:end, :].squeeze(0)
sample_hidden_state.append(tensor)
return sample_hidden_state
# Stay packed: the projector slices per image on the host, so splitting
# here would index `cu_seqlens` on the device and stall once per image.
return self.post_layernorm(hidden_states).squeeze(0)
class SiglipVisionModel(nn.Module):
@@ -570,48 +558,38 @@ class SiglipVisionModel(nn.Module):
def forward(
self,
pixel_values,
interpolate_pos_encoding: bool = False,
pixel_values: torch.Tensor,
grid_thws: List[GridTHW],
cu_seqlens: torch.Tensor,
max_seqlen: int,
position_ids: Optional[torch.Tensor] = None,
image_grid_thw: Optional[
List[
Union[
Tuple[int, int, int],
List[Tuple[int, int, int]],
]
]
] = None,
cu_seqlens: Optional[List[torch.Tensor]] = None,
) -> list[torch.Tensor]:
) -> torch.Tensor:
return self.vision_model(
pixel_values=pixel_values,
interpolate_pos_encoding=interpolate_pos_encoding,
position_ids=position_ids,
image_grid_thw=image_grid_thw,
grid_thws=grid_thws,
cu_seqlens=cu_seqlens,
max_seqlen=max_seqlen,
position_ids=position_ids,
)
class PaddleOCRVLForConditionalGeneration(Ernie4_5_ForCausalLM):
def __init__(self, *, config, quant_config=None, prefix: str = ""):
super().__init__(config=config, prefix=prefix)
super().__init__(config=config, quant_config=quant_config, prefix=prefix)
config = self.config
self.mlp_AR = Projector(
config, config.vision_config, prefix=add_prefix("mlp_AR", prefix)
)
# NOTE: only BitsAndBytes 4-bit quantization is exercised for the SigLIP
# tower; other methods fall back to bf16 through SiglipMLP's own gate.
self.visual = SiglipVisionModel(
config=config.vision_config, prefix=add_prefix("visual", prefix)
config=config.vision_config,
quant_config=quant_config,
prefix=add_prefix("visual", prefix),
)
if not hasattr(self.model, "get_input_embeddings"):
import types
self.model.get_input_embeddings = types.MethodType(
get_input_embeddings, self.model
)
self.is_mrope_enabled = "mrope_section" in self.config.rope_scaling
self.is_mrope_enabled = "mrope_section" in (self.config.rope_scaling or {})
def pad_input_ids(self, input_ids: List[int], mm_inputs: MultimodalInputs):
pattern = MultiModalityDataPaddingPatternMultimodalTokens()
@@ -620,46 +598,38 @@ class PaddleOCRVLForConditionalGeneration(Ernie4_5_ForCausalLM):
def get_input_embeddings(self):
return self.model.embed_tokens
def encode_image(self, pixel_values, image_grid_thw):
pixel_values = pixel_values.type(self.visual.dtype)
siglip_position_ids = list()
image_grid_hws = list()
cu_seqlens = [0]
for idx, grid_thw in enumerate(image_grid_thw):
thw_tuple = tuple(grid_thw.detach().cpu().numpy().tolist())
numel = np.prod(thw_tuple)
image_grid_hws.append(thw_tuple)
image_position_ids = torch.arange(numel) % np.prod(thw_tuple[1:])
siglip_position_ids.append(image_position_ids)
cu_seqlens.append(cu_seqlens[-1] + numel)
siglip_position_ids = torch.concat(siglip_position_ids, dim=0).to(
pixel_values.device
def encode_image(
self, pixel_values: torch.Tensor, image_grid_thw: torch.Tensor
) -> torch.Tensor:
# One host transfer for the whole batch. Every consumer of the grids
# (rope table, patch merge, cu_seqlens) needs them on the host, so
# reading them per image would cost one synchronization per image.
grid_thws: List[GridTHW] = [(t, h, w) for t, h, w in image_grid_thw.tolist()]
seq_lens = [t * h * w for t, h, w in grid_thws]
cu_seqlens = torch.tensor(
[0, *itertools.accumulate(seq_lens)],
dtype=torch.int32,
device=pixel_values.device,
)
cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int32).to(pixel_values.device)
vision_outputs = self.visual(
pixel_values=pixel_values,
image_grid_thw=image_grid_hws,
position_ids=siglip_position_ids,
interpolate_pos_encoding=True,
grid_thws=grid_thws,
cu_seqlens=cu_seqlens,
max_seqlen=max(seq_lens),
)
image_embeds = self.mlp_AR(vision_outputs, image_grid_thw)
# image_embeds = torch.stack(image_embeds, dim=0)
image_embeds = torch.cat(image_embeds, dim=0)
return image_embeds
return self.mlp_AR(vision_outputs, grid_thws)
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
pixel_values = torch.cat([item.feature for item in items], dim=0).type(
self.visual.dtype
)
image_grid_thw = torch.concat([item.image_grid_thw for item in items], dim=0)
image_embeds = self.encode_image(pixel_values, image_grid_thw)
return image_embeds
if len(items) == 1:
# torch.cat allocates even for a single input; a document batch is
# usually one image, and its pixel buffer is the largest tensor here.
pixel_values = items[0].feature
image_grid_thw = items[0].image_grid_thw
else:
pixel_values = torch.cat([item.feature for item in items], dim=0)
image_grid_thw = torch.cat([item.image_grid_thw for item in items], dim=0)
return self.encode_image(pixel_values, image_grid_thw)
def forward(
self,
@@ -670,11 +640,10 @@ class PaddleOCRVLForConditionalGeneration(Ernie4_5_ForCausalLM):
):
if self.is_mrope_enabled:
positions = forward_batch.mrope_positions
if not (
forward_batch.forward_mode.is_decode()
or not forward_batch.contains_image_inputs()
):
if self.is_mrope_enabled:
if (
not forward_batch.forward_mode.is_decode()
and forward_batch.contains_image_inputs()
):
assert positions.ndim == 2 and positions.size(0) == 3, (
"multimodal section rotary embedding requires "
f"(3, seq_len) positions, but got {positions.size()}"
@@ -732,9 +701,4 @@ class PaddleOCRVLForConditionalGeneration(Ernie4_5_ForCausalLM):
raise KeyError(f"Parameter '{name}' not found in model.")
# monkey patch
def get_input_embeddings(self) -> nn.Embedding:
return self.embed_tokens
EntryClass = [PaddleOCRVLForConditionalGeneration]
@@ -20,6 +20,21 @@ from sglang.srt.multimodal.processors.qwen_vl import QwenVLImageProcessor
class PaddleOCRVLImageProcessor(QwenVLImageProcessor):
models = [PaddleOCRVLForConditionalGeneration]
# A document page is a far heavier preprocessing unit than a chat image:
# resize + normalize + patchify of a full-resolution scan costs tens of
# milliseconds, so a single worker caps request throughput at
# 1 / preprocess_time regardless of how much GPU is left idle. Overlap it
# across workers; the work itself is unchanged.
#
# Two, not more: measured on an H200 at 32-way concurrency, two workers beat
# both one and four on every shape tried (1080p pages 6.72 -> 9.55 req/s at
# two, 8.92 at four; 360p pages with 512-token outputs 22.38 -> 25.20 at
# two, 25.06 at four). Past two, spreading request arrivals fragments the
# GPU prefill batches faster than the extra overlap pays for itself.
auto_mm_processor_worker_num = 2
auto_mm_io_worker_num = 16
supports_mm_processor_concurrency = True
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
super().__init__(hf_config, server_args, _processor, *args, **kwargs)