Instructions to use PaddlePaddle/PaddleOCR-VL with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PaddleOCR
How to use PaddlePaddle/PaddleOCR-VL with PaddleOCR:
# See https://www.paddleocr.ai/latest/version3.x/pipeline_usage/PaddleOCR-VL.html to installation from paddleocr import PaddleOCRVL pipeline = PaddleOCRVL(pipeline_version="v1") output = pipeline.predict("path/to/document_image.png") for res in output: res.print() res.save_to_json(save_path="output") res.save_to_markdown(save_path="output") - Notebooks
- Google Colab
- Kaggle
Fix remote-code compatibility across Transformers 4.x and 5.x
Browse filesThis draft ports the remote-code compatibility fix to **PaddlePaddle/PaddleOCR-VL**. It preserves `AutoModelForCausalLM`, `AutoProcessor` and `trust_remote_code=True`, without requiring a pinned Transformers release.
The unmodified source was reproduced on meta storage, without loading checkpoint weights: Transformers 4.57.6 reaches the text forward and raises `create_causal_mask() got an unexpected keyword argument 'inputs_embeds'`; 5.0.0 and 5.21.1 fail during construction with `KeyError: 'default'`. The architecture/configuration and affected implementation are shared with 1.6 (1.0 differs only in import order).
The patch:
- computes default RoPE frequencies locally;
- builds causal/padding/4-D boolean masks locally, avoiding the changing Transformers mask helper interface;
- obtains generation positions from prepared inputs or cache length and preserves images for uncached generation;
- restores derived non-persistent RoPE/position buffers and protects loaded parameters during custom initialization;
- isolates non-FlashAttention packed vision attention by image, preventing cross-image contamination;
- includes a reproducible checkpoint regression script, three fixtures, and this model's own validation summary.
### Validation
Processor loading and meta-device model construction passed. **Full-checkpoint regression is in progress and is not claimed here.**
| Transformers | Processor + meta construction |
| --- | --- |
| 4.57.6 | Passed |
| 5.0.0 | Passed |
| 5.21.1 | Passed |
```bash
python -m pip install einops sentencepiece protobuf
python tests/test_remote_code_compat.py --full --model . --inputs tests/data --output /tmp/paddleocr-vl-compat
```
Only the listed executions are claimed. CUDA/MPS, low precision, FlashAttention, StaticCache, training, dataset-level accuracy/performance, and every historical/intermediate/future Transformers release are not verified here. Full-checkpoint results for another model are not substituted for this model's results.
Related 1.6 draft: https://huggingface.co/PaddlePaddle/PaddleOCR-VL-1.6/discussions/15 . The final patched model source is byte-identical across 1.0, 1.5 and 1.6 (SHA-256 `d01a9847b5e4142e5aa35c28f2354fae920ab31b2f990216db39287d344758ce`).
- modeling_paddleocr_vl.py +135 -26
- tests/README.md +22 -0
- tests/compatibility_results.json +76 -0
- tests/data/invoice_en.png +0 -0
- tests/data/paddle_general_ocr_001.png +0 -0
- tests/data/table_en.png +0 -0
- tests/test_remote_code_compat.py +159 -0
|
@@ -12,6 +12,7 @@
|
|
| 12 |
# See the License for the specific language governing permissions and
|
| 13 |
# limitations under the License.
|
| 14 |
|
|
|
|
| 15 |
import math
|
| 16 |
import warnings
|
| 17 |
from dataclasses import dataclass
|
|
@@ -30,7 +31,6 @@ from transformers.cache_utils import (
|
|
| 30 |
)
|
| 31 |
from transformers.generation import GenerationMixin
|
| 32 |
from transformers.integrations import use_kernel_forward_from_hub
|
| 33 |
-
from transformers.masking_utils import create_causal_mask
|
| 34 |
from transformers.modeling_attn_mask_utils import AttentionMaskConverter
|
| 35 |
from transformers.modeling_layers import GradientCheckpointingLayer
|
| 36 |
from transformers.modeling_outputs import (
|
|
@@ -66,6 +66,62 @@ else:
|
|
| 66 |
from .configuration_paddleocr_vl import PaddleOCRVisionConfig, PaddleOCRVLConfig
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
class RotaryEmbedding(nn.Module):
|
| 70 |
def __init__(self, config: PaddleOCRVLConfig, device=None):
|
| 71 |
super().__init__()
|
|
@@ -92,12 +148,25 @@ class RotaryEmbedding(nn.Module):
|
|
| 92 |
self.original_max_seq_len = config.max_position_embeddings
|
| 93 |
|
| 94 |
self.config = config
|
| 95 |
-
self.
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device)
|
| 98 |
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
| 99 |
self.original_inv_freq = self.inv_freq
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
def _dynamic_frequency_update(self, position_ids, device):
|
| 102 |
"""
|
| 103 |
dynamic RoPE layers should recompute `inv_freq` in the following situations:
|
|
@@ -539,6 +608,12 @@ class Ernie4_5PreTrainedModel(PreTrainedModel):
|
|
| 539 |
}
|
| 540 |
|
| 541 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
@auto_docstring
|
| 543 |
class Ernie4_5Model(Ernie4_5PreTrainedModel):
|
| 544 |
def __init__(self, config: PaddleOCRVLConfig):
|
|
@@ -603,13 +678,8 @@ class Ernie4_5Model(Ernie4_5PreTrainedModel):
|
|
| 603 |
elif position_ids.dim() == 2:
|
| 604 |
position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
|
| 605 |
|
| 606 |
-
causal_mask =
|
| 607 |
-
|
| 608 |
-
inputs_embeds=inputs_embeds,
|
| 609 |
-
attention_mask=attention_mask,
|
| 610 |
-
past_key_values=past_key_values,
|
| 611 |
-
position_ids=position_ids,
|
| 612 |
-
cache_position=cache_position,
|
| 613 |
)
|
| 614 |
|
| 615 |
hidden_states = inputs_embeds
|
|
@@ -712,6 +782,8 @@ class Ernie4_5ForCausalLM(Ernie4_5PreTrainedModel, GenerationMixin):
|
|
| 712 |
|
| 713 |
|
| 714 |
def _trunc_normal_(tensor, mean, std, a, b):
|
|
|
|
|
|
|
| 715 |
# Cut & paste from PyTorch official master until it's in a few official releases - RW
|
| 716 |
# Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
|
| 717 |
def norm_cdf(x):
|
|
@@ -772,12 +844,16 @@ def trunc_normal_tf_(
|
|
| 772 |
a: the minimum cutoff value
|
| 773 |
b: the maximum cutoff value
|
| 774 |
"""
|
|
|
|
|
|
|
| 775 |
with torch.no_grad():
|
| 776 |
_trunc_normal_(tensor, 0, 1.0, a, b)
|
| 777 |
tensor.mul_(std).add_(mean)
|
| 778 |
|
| 779 |
|
| 780 |
def variance_scaling_(tensor, scale=1.0, mode="fan_in", distribution="normal"):
|
|
|
|
|
|
|
| 781 |
fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor)
|
| 782 |
if mode == "fan_in":
|
| 783 |
denom = fan_in
|
|
@@ -1134,16 +1210,34 @@ class PaddleOCRAttention(nn.Module):
|
|
| 1134 |
elif self.config._attn_implementation == "sdpa":
|
| 1135 |
attention_interface = sdpa_attention_forward
|
| 1136 |
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1147 |
attn_output = attn_output.reshape(
|
| 1148 |
batch_size, seq_length, embed_dim
|
| 1149 |
).contiguous()
|
|
@@ -1267,7 +1361,8 @@ class PaddleOCRPreTrainedModel(PreTrainedModel):
|
|
| 1267 |
_supports_sdpa = True
|
| 1268 |
|
| 1269 |
def _init_weights(self, module):
|
| 1270 |
-
"""Initialize the weights"""
|
|
|
|
| 1271 |
if isinstance(module, PaddleOCRVisionEmbeddings):
|
| 1272 |
width = (
|
| 1273 |
self.config.vision_config.hidden_size
|
|
@@ -1292,16 +1387,16 @@ class PaddleOCRPreTrainedModel(PreTrainedModel):
|
|
| 1292 |
nn.init.normal_(module.fc1.bias, std=1e-6)
|
| 1293 |
nn.init.normal_(module.fc2.bias, std=1e-6)
|
| 1294 |
elif isinstance(module, PaddleOCRMultiheadAttentionPoolingHead):
|
| 1295 |
-
nn.init.xavier_uniform_(module.probe
|
| 1296 |
-
nn.init.xavier_uniform_(module.attention.in_proj_weight
|
| 1297 |
-
nn.init.zeros_(module.attention.in_proj_bias
|
| 1298 |
elif isinstance(module, (nn.Linear, nn.Conv2d)):
|
| 1299 |
lecun_normal_(module.weight)
|
| 1300 |
if module.bias is not None:
|
| 1301 |
nn.init.zeros_(module.bias)
|
| 1302 |
elif isinstance(module, nn.LayerNorm):
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
|
| 1306 |
|
| 1307 |
# Copied from transformers.models.altclip.modeling_altclip.AltCLIPEncoder with AltCLIP->PaddleOCR
|
|
@@ -2342,7 +2437,21 @@ class PaddleOCRVLForConditionalGeneration(Ernie4_5PreTrainedModel, GenerationMix
|
|
| 2342 |
|
| 2343 |
model_inputs["position_ids"] = None
|
| 2344 |
|
| 2345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2346 |
model_inputs["pixel_values"] = None
|
| 2347 |
model_inputs["pixel_values_videos"] = None
|
| 2348 |
|
|
|
|
| 12 |
# See the License for the specific language governing permissions and
|
| 13 |
# limitations under the License.
|
| 14 |
|
| 15 |
+
import inspect
|
| 16 |
import math
|
| 17 |
import warnings
|
| 18 |
from dataclasses import dataclass
|
|
|
|
| 31 |
)
|
| 32 |
from transformers.generation import GenerationMixin
|
| 33 |
from transformers.integrations import use_kernel_forward_from_hub
|
|
|
|
| 34 |
from transformers.modeling_attn_mask_utils import AttentionMaskConverter
|
| 35 |
from transformers.modeling_layers import GradientCheckpointingLayer
|
| 36 |
from transformers.modeling_outputs import (
|
|
|
|
| 66 |
from .configuration_paddleocr_vl import PaddleOCRVisionConfig, PaddleOCRVLConfig
|
| 67 |
|
| 68 |
|
| 69 |
+
def _create_causal_mask(config, inputs_embeds, attention_mask, past_key_values, cache_position):
|
| 70 |
+
# Keep mask semantics local: Transformers renamed inputs and removed
|
| 71 |
+
# cache_position across releases. M-RoPE coordinates are handled separately.
|
| 72 |
+
if attention_mask is not None and attention_mask.ndim == 4:
|
| 73 |
+
mask = attention_mask.to(device=inputs_embeds.device)
|
| 74 |
+
if mask.dtype == torch.bool:
|
| 75 |
+
additive = torch.zeros(mask.shape, device=mask.device, dtype=inputs_embeds.dtype)
|
| 76 |
+
return additive.masked_fill(~mask, torch.finfo(inputs_embeds.dtype).min)
|
| 77 |
+
return mask.to(dtype=inputs_embeds.dtype)
|
| 78 |
+
if config._attn_implementation == "flash_attention_2":
|
| 79 |
+
return attention_mask if attention_mask is not None and (attention_mask == 0).any() else None
|
| 80 |
+
batch_size, query_length = inputs_embeds.shape[:2]
|
| 81 |
+
past_length = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 82 |
+
key_length = past_length + query_length
|
| 83 |
+
if attention_mask is not None:
|
| 84 |
+
key_length = max(key_length, attention_mask.shape[-1])
|
| 85 |
+
if past_key_values is not None and getattr(past_key_values, "is_compileable", False):
|
| 86 |
+
maximum = past_key_values.get_max_cache_shape()
|
| 87 |
+
if isinstance(maximum, int):
|
| 88 |
+
key_length = max(key_length, maximum)
|
| 89 |
+
query_positions = cache_position[-query_length:].to(inputs_embeds.device)
|
| 90 |
+
key_positions = torch.arange(key_length, device=inputs_embeds.device)
|
| 91 |
+
allowed = key_positions[None, :] <= query_positions[:, None]
|
| 92 |
+
allowed = allowed[None, None, :, :].expand(batch_size, 1, query_length, key_length)
|
| 93 |
+
if attention_mask is not None:
|
| 94 |
+
padding = attention_mask[:, :key_length].to(device=inputs_embeds.device, dtype=torch.bool)
|
| 95 |
+
if padding.shape[-1] < key_length:
|
| 96 |
+
padding = F.pad(padding, (0, key_length - padding.shape[-1]), value=False)
|
| 97 |
+
allowed = allowed & padding[:, None, None, :]
|
| 98 |
+
mask = torch.zeros(allowed.shape, dtype=inputs_embeds.dtype, device=inputs_embeds.device)
|
| 99 |
+
return mask.masked_fill(~allowed, torch.finfo(inputs_embeds.dtype).min)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _init_derived_buffers(module):
|
| 103 |
+
# v5 materializes non-persistent buffers from meta storage; they are not
|
| 104 |
+
# loaded from the checkpoint and must be initialized explicitly.
|
| 105 |
+
if isinstance(module, RotaryEmbedding):
|
| 106 |
+
inv_freq, module.attention_scaling = module.rope_init_fn(
|
| 107 |
+
module.config, device=module.inv_freq.device
|
| 108 |
+
)
|
| 109 |
+
module.inv_freq.copy_(inv_freq)
|
| 110 |
+
module.original_inv_freq = module.inv_freq
|
| 111 |
+
elif isinstance(module, SigLIPRotaryEmbedding):
|
| 112 |
+
inv_freq = 1.0 / (
|
| 113 |
+
module.theta ** (
|
| 114 |
+
torch.arange(0, module.dim, 2, dtype=torch.float32, device=module.inv_freq.device)
|
| 115 |
+
/ module.dim
|
| 116 |
+
)
|
| 117 |
+
)
|
| 118 |
+
module.inv_freq.copy_(inv_freq)
|
| 119 |
+
elif isinstance(module, PaddleOCRVisionEmbeddings):
|
| 120 |
+
module.position_ids.copy_(
|
| 121 |
+
torch.arange(module.num_positions, device=module.position_ids.device).expand(1, -1)
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
class RotaryEmbedding(nn.Module):
|
| 126 |
def __init__(self, config: PaddleOCRVLConfig, device=None):
|
| 127 |
super().__init__()
|
|
|
|
| 148 |
self.original_max_seq_len = config.max_position_embeddings
|
| 149 |
|
| 150 |
self.config = config
|
| 151 |
+
if self.rope_type == "default":
|
| 152 |
+
self.rope_init_fn = self.compute_default_rope_parameters
|
| 153 |
+
else:
|
| 154 |
+
self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
|
| 155 |
|
| 156 |
inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device)
|
| 157 |
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
| 158 |
self.original_inv_freq = self.inv_freq
|
| 159 |
|
| 160 |
+
@staticmethod
|
| 161 |
+
def compute_default_rope_parameters(config, device=None, seq_len=None):
|
| 162 |
+
# Default RoPE is no longer part of ROPE_INIT_FUNCTIONS in v5.
|
| 163 |
+
head_dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads
|
| 164 |
+
dim = int(head_dim * getattr(config, "partial_rotary_factor", 1.0))
|
| 165 |
+
inv_freq = 1.0 / (
|
| 166 |
+
config.rope_theta ** (torch.arange(0, dim, 2, dtype=torch.float32, device=device) / dim)
|
| 167 |
+
)
|
| 168 |
+
return inv_freq, 1.0
|
| 169 |
+
|
| 170 |
def _dynamic_frequency_update(self, position_ids, device):
|
| 171 |
"""
|
| 172 |
dynamic RoPE layers should recompute `inv_freq` in the following situations:
|
|
|
|
| 608 |
}
|
| 609 |
|
| 610 |
|
| 611 |
+
@torch.no_grad()
|
| 612 |
+
def _init_weights(self, module):
|
| 613 |
+
super()._init_weights(module)
|
| 614 |
+
_init_derived_buffers(module)
|
| 615 |
+
|
| 616 |
+
|
| 617 |
@auto_docstring
|
| 618 |
class Ernie4_5Model(Ernie4_5PreTrainedModel):
|
| 619 |
def __init__(self, config: PaddleOCRVLConfig):
|
|
|
|
| 678 |
elif position_ids.dim() == 2:
|
| 679 |
position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
|
| 680 |
|
| 681 |
+
causal_mask = _create_causal_mask(
|
| 682 |
+
self.config, inputs_embeds, attention_mask, past_key_values, cache_position
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
)
|
| 684 |
|
| 685 |
hidden_states = inputs_embeds
|
|
|
|
| 782 |
|
| 783 |
|
| 784 |
def _trunc_normal_(tensor, mean, std, a, b):
|
| 785 |
+
if getattr(tensor, "_is_hf_initialized", False):
|
| 786 |
+
return tensor
|
| 787 |
# Cut & paste from PyTorch official master until it's in a few official releases - RW
|
| 788 |
# Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
|
| 789 |
def norm_cdf(x):
|
|
|
|
| 844 |
a: the minimum cutoff value
|
| 845 |
b: the maximum cutoff value
|
| 846 |
"""
|
| 847 |
+
if getattr(tensor, "_is_hf_initialized", False):
|
| 848 |
+
return tensor
|
| 849 |
with torch.no_grad():
|
| 850 |
_trunc_normal_(tensor, 0, 1.0, a, b)
|
| 851 |
tensor.mul_(std).add_(mean)
|
| 852 |
|
| 853 |
|
| 854 |
def variance_scaling_(tensor, scale=1.0, mode="fan_in", distribution="normal"):
|
| 855 |
+
if getattr(tensor, "_is_hf_initialized", False):
|
| 856 |
+
return tensor
|
| 857 |
fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor)
|
| 858 |
if mode == "fan_in":
|
| 859 |
denom = fan_in
|
|
|
|
| 1210 |
elif self.config._attn_implementation == "sdpa":
|
| 1211 |
attention_interface = sdpa_attention_forward
|
| 1212 |
|
| 1213 |
+
if cu_seqlens is not None and cu_seqlens.numel() > 2:
|
| 1214 |
+
boundaries = cu_seqlens.tolist()
|
| 1215 |
+
outputs = []
|
| 1216 |
+
attn_weights = (
|
| 1217 |
+
queries.new_zeros(batch_size, self.num_heads, seq_length, seq_length)
|
| 1218 |
+
if output_attentions else None
|
| 1219 |
+
)
|
| 1220 |
+
for start, end in zip(boundaries[:-1], boundaries[1:]):
|
| 1221 |
+
local_mask = (
|
| 1222 |
+
attention_mask[..., start:end, start:end]
|
| 1223 |
+
if attention_mask is not None else None
|
| 1224 |
+
)
|
| 1225 |
+
output, weights = attention_interface(
|
| 1226 |
+
self, queries[:, :, start:end], keys[:, :, start:end],
|
| 1227 |
+
values[:, :, start:end], local_mask,
|
| 1228 |
+
is_causal=False, scaling=self.scale,
|
| 1229 |
+
dropout=0.0 if not self.training else self.dropout,
|
| 1230 |
+
)
|
| 1231 |
+
outputs.append(output)
|
| 1232 |
+
if attn_weights is not None and weights is not None:
|
| 1233 |
+
attn_weights[..., start:end, start:end] = weights
|
| 1234 |
+
attn_output = torch.cat(outputs, dim=1)
|
| 1235 |
+
else:
|
| 1236 |
+
attn_output, attn_weights = attention_interface(
|
| 1237 |
+
self, queries, keys, values, attention_mask,
|
| 1238 |
+
is_causal=self.is_causal, scaling=self.scale,
|
| 1239 |
+
dropout=0.0 if not self.training else self.dropout,
|
| 1240 |
+
)
|
| 1241 |
attn_output = attn_output.reshape(
|
| 1242 |
batch_size, seq_length, embed_dim
|
| 1243 |
).contiguous()
|
|
|
|
| 1361 |
_supports_sdpa = True
|
| 1362 |
|
| 1363 |
def _init_weights(self, module):
|
| 1364 |
+
"""Initialize the weights and derived non-persistent buffers."""
|
| 1365 |
+
_init_derived_buffers(module)
|
| 1366 |
if isinstance(module, PaddleOCRVisionEmbeddings):
|
| 1367 |
width = (
|
| 1368 |
self.config.vision_config.hidden_size
|
|
|
|
| 1387 |
nn.init.normal_(module.fc1.bias, std=1e-6)
|
| 1388 |
nn.init.normal_(module.fc2.bias, std=1e-6)
|
| 1389 |
elif isinstance(module, PaddleOCRMultiheadAttentionPoolingHead):
|
| 1390 |
+
nn.init.xavier_uniform_(module.probe)
|
| 1391 |
+
nn.init.xavier_uniform_(module.attention.in_proj_weight)
|
| 1392 |
+
nn.init.zeros_(module.attention.in_proj_bias)
|
| 1393 |
elif isinstance(module, (nn.Linear, nn.Conv2d)):
|
| 1394 |
lecun_normal_(module.weight)
|
| 1395 |
if module.bias is not None:
|
| 1396 |
nn.init.zeros_(module.bias)
|
| 1397 |
elif isinstance(module, nn.LayerNorm):
|
| 1398 |
+
nn.init.zeros_(module.bias)
|
| 1399 |
+
nn.init.ones_(module.weight)
|
| 1400 |
|
| 1401 |
|
| 1402 |
# Copied from transformers.models.altclip.modeling_altclip.AltCLIPEncoder with AltCLIP->PaddleOCR
|
|
|
|
| 2437 |
|
| 2438 |
model_inputs["position_ids"] = None
|
| 2439 |
|
| 2440 |
+
# v5 creates cache_position inside the parent method, rather than
|
| 2441 |
+
# passing it to this override. Read the prepared value back first.
|
| 2442 |
+
cache_position = model_inputs.get("cache_position", cache_position)
|
| 2443 |
+
if cache_position is None:
|
| 2444 |
+
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
|
| 2445 |
+
prepared = model_inputs.get("inputs_embeds")
|
| 2446 |
+
if prepared is None:
|
| 2447 |
+
prepared = model_inputs["input_ids"]
|
| 2448 |
+
cache_position = torch.arange(
|
| 2449 |
+
past_seen_tokens, past_seen_tokens + prepared.shape[1], device=prepared.device
|
| 2450 |
+
)
|
| 2451 |
+
model_inputs["cache_position"] = cache_position
|
| 2452 |
+
|
| 2453 |
+
# Without KV caching, every forward still consumes the image tokens.
|
| 2454 |
+
if use_cache and past_key_values is not None and past_key_values.get_seq_length() > 0:
|
| 2455 |
model_inputs["pixel_values"] = None
|
| 2456 |
model_inputs["pixel_values_videos"] = None
|
| 2457 |
|
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Remote-code compatibility regression
|
| 2 |
+
|
| 3 |
+
Run from a model checkout containing the checkpoint and tokenizer files:
|
| 4 |
+
|
| 5 |
+
```bash
|
| 6 |
+
python -m pip install einops sentencepiece protobuf
|
| 7 |
+
python tests/test_remote_code_compat.py --full --model . --inputs tests/data --output /tmp/paddleocr-vl-compat
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
Use an environment with PyTorch, torchvision, Pillow, NumPy, safetensors, and the Transformers version under test. The script always uses `trust_remote_code=True`, local checkpoint files, CPU float32, and an isolated dynamic-module cache below the output directory. Each version is a separate process/environment; framework versions are not switched inside one Python process.
|
| 11 |
+
|
| 12 |
+
The default suite checks checkpoint parameter equality, derived RoPE buffers, mask semantics, English generation, an uncached prefix, and batch-prefill isolation. `--full` additionally checks a complete uncached English generation, Chinese OCR, equality of individual/batched decoded text, and table recognition.
|
| 13 |
+
|
| 14 |
+
`compatibility_results.json` records the three executed boundary versions for this model and their environment scope. It does not claim continuous-range or future-version coverage.
|
| 15 |
+
|
| 16 |
+
Fixtures:
|
| 17 |
+
|
| 18 |
+
- `paddle_general_ocr_001.png`: official synthetic Chinese OCR test image from https://huggingface.co/datasets/hf-internal-testing/transformers-synthetic-assets/resolve/main/images/paddle_general_ocr_001.png .
|
| 19 |
+
- `invoice_en.png`: synthetic three-line English invoice with known expected text.
|
| 20 |
+
- `table_en.png`: synthetic Item/Qty/Price table containing Apple and Orange rows.
|
| 21 |
+
|
| 22 |
+
Draft progress: only processor loading and meta-device construction have passed for this model so far. Full-checkpoint regression is pending; do not interpret the summary as a generation test.
|
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "PaddlePaddle/PaddleOCR-VL",
|
| 3 |
+
"revision": "7fa00a8c55b735ba51ba49a9058f3f9c57a99a11",
|
| 4 |
+
"trust_remote_code": true,
|
| 5 |
+
"validation_scope": "meta construction and processor only",
|
| 6 |
+
"full_checkpoint_pending": true,
|
| 7 |
+
"device": "meta",
|
| 8 |
+
"dtype": null,
|
| 9 |
+
"checkpoint_sha256": null,
|
| 10 |
+
"tested_versions": [
|
| 11 |
+
"4.57.6",
|
| 12 |
+
"5.0.0",
|
| 13 |
+
"5.21.1"
|
| 14 |
+
],
|
| 15 |
+
"same_source_sha256": {
|
| 16 |
+
"modeling_paddleocr_vl.py": "d01a9847b5e4142e5aa35c28f2354fae920ab31b2f990216db39287d344758ce",
|
| 17 |
+
"image_processing_paddleocr_vl.py": "a4fa521b9cb16e207f94b7f2d16427771776dfc634420d319fc4916ee58049ec",
|
| 18 |
+
"processing_paddleocr_vl.py": "e29cb1e5f275f2bd3ce051bd5c9983a33894e693b2823a0e13d4c07c8c4f9e13",
|
| 19 |
+
"configuration_paddleocr_vl.py": "753dd93654c3a9c8c85a3eaee1e3092dd12591b0f2dce0305e1abfb7a41ff160"
|
| 20 |
+
},
|
| 21 |
+
"all_tested_passed": true,
|
| 22 |
+
"universal_historical_or_future_support_claimed": false,
|
| 23 |
+
"original_reproduction_scope": "original source, meta construction/text forward; no checkpoint weights",
|
| 24 |
+
"original_failures": [
|
| 25 |
+
{
|
| 26 |
+
"transformers": "4.57.6",
|
| 27 |
+
"stage": "text_decoder_forward",
|
| 28 |
+
"exception_type": "TypeError",
|
| 29 |
+
"error": "create_causal_mask() got an unexpected keyword argument 'inputs_embeds'",
|
| 30 |
+
"expected_failure": true
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"transformers": "5.0.0",
|
| 34 |
+
"stage": null,
|
| 35 |
+
"exception_type": "KeyError",
|
| 36 |
+
"error": "'default'",
|
| 37 |
+
"expected_failure": true
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"transformers": "5.21.1",
|
| 41 |
+
"stage": null,
|
| 42 |
+
"exception_type": "KeyError",
|
| 43 |
+
"error": "'default'",
|
| 44 |
+
"expected_failure": true
|
| 45 |
+
}
|
| 46 |
+
],
|
| 47 |
+
"versions": [
|
| 48 |
+
{
|
| 49 |
+
"version": "4.57.6",
|
| 50 |
+
"passed": true,
|
| 51 |
+
"stages": [
|
| 52 |
+
"config",
|
| 53 |
+
"processor",
|
| 54 |
+
"model_class"
|
| 55 |
+
]
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"version": "5.0.0",
|
| 59 |
+
"passed": true,
|
| 60 |
+
"stages": [
|
| 61 |
+
"config",
|
| 62 |
+
"processor",
|
| 63 |
+
"model_class"
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"version": "5.21.1",
|
| 68 |
+
"passed": true,
|
| 69 |
+
"stages": [
|
| 70 |
+
"config",
|
| 71 |
+
"processor",
|
| 72 |
+
"model_class"
|
| 73 |
+
]
|
| 74 |
+
}
|
| 75 |
+
]
|
| 76 |
+
}
|
|
|
|
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run unchanged API calls against one selected Transformers installation."""
|
| 2 |
+
import argparse
|
| 3 |
+
import importlib.metadata
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
import traceback
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
args_parser=argparse.ArgumentParser()
|
| 12 |
+
args_parser.add_argument('--imports-only',action='store_true')
|
| 13 |
+
args_parser.add_argument('--full',action='store_true')
|
| 14 |
+
args_parser.add_argument('--model',type=Path)
|
| 15 |
+
args_parser.add_argument('--inputs',type=Path)
|
| 16 |
+
args_parser.add_argument('--output',type=Path)
|
| 17 |
+
args=args_parser.parse_args()
|
| 18 |
+
TASK=Path(__file__).resolve().parent
|
| 19 |
+
ROOT=TASK.parent
|
| 20 |
+
VERSION=importlib.metadata.version('transformers')
|
| 21 |
+
MODEL=args.model or ROOT
|
| 22 |
+
INPUTS=args.inputs or TASK/'data'
|
| 23 |
+
OUT=(args.output or TASK/'results')/VERSION
|
| 24 |
+
OUT.mkdir(parents=True,exist_ok=True)
|
| 25 |
+
os.environ['HF_MODULES_CACHE']=str(OUT/'modules')
|
| 26 |
+
os.environ['HF_HUB_OFFLINE']='1'
|
| 27 |
+
os.environ['TOKENIZERS_PARALLELISM']='false'
|
| 28 |
+
report={'transformers':VERSION,'trust_remote_code':True,'mode':'imports' if args.imports_only else 'full' if args.full else 'matrix','pid':os.getpid(),'started_at':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'stages':{}}
|
| 29 |
+
report['source_sha256']={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in MODEL.glob('*.py')}
|
| 30 |
+
def save(): (OUT/('imports.json' if args.imports_only else 'full.json' if args.full else 'results.json')).write_text(json.dumps(report,ensure_ascii=False,indent=2,default=str)+'\n')
|
| 31 |
+
def step(name,fn):
|
| 32 |
+
started=time.perf_counter()
|
| 33 |
+
try:
|
| 34 |
+
result=fn();report['stages'][name]={'passed':True,'seconds':round(time.perf_counter()-started,3)}
|
| 35 |
+
print(name,'PASS',flush=True);save();return result
|
| 36 |
+
except Exception as exc:
|
| 37 |
+
report['stages'][name]={'passed':False,'error':str(exc),'traceback':traceback.format_exc()}
|
| 38 |
+
print(name,type(exc).__name__,str(exc),flush=True);save();raise
|
| 39 |
+
try:
|
| 40 |
+
import torch,transformers,tokenizers,huggingface_hub
|
| 41 |
+
from PIL import Image
|
| 42 |
+
from transformers import AutoConfig,AutoModelForCausalLM,AutoProcessor
|
| 43 |
+
from transformers.utils import logging
|
| 44 |
+
logging.disable_progress_bar()
|
| 45 |
+
torch.set_num_threads(4)
|
| 46 |
+
report['packages']={name:importlib.metadata.version(name) for name in ['transformers','torch','tokenizers','huggingface-hub','safetensors','pillow']}
|
| 47 |
+
report['transformers_source']=transformers.__file__
|
| 48 |
+
common={'trust_remote_code':True,'local_files_only':True}
|
| 49 |
+
config=step('config',lambda:AutoConfig.from_pretrained(MODEL,**common))
|
| 50 |
+
processor=step('processor',lambda:AutoProcessor.from_pretrained(MODEL,**common))
|
| 51 |
+
def construct():
|
| 52 |
+
with torch.device('meta'):
|
| 53 |
+
return AutoModelForCausalLM.from_config(config,trust_remote_code=True)
|
| 54 |
+
if args.imports_only:
|
| 55 |
+
model=step('model_class',construct)
|
| 56 |
+
else:
|
| 57 |
+
def load():
|
| 58 |
+
model,info=AutoModelForCausalLM.from_pretrained(MODEL,torch_dtype=torch.float32,output_loading_info=True,**common)
|
| 59 |
+
report['loading_info']={k:sorted(v) if isinstance(v,set) else v for k,v in info.items()}
|
| 60 |
+
assert not info.get('missing_keys') and not info.get('mismatched_keys'),info
|
| 61 |
+
report['model_class']=type(model).__module__+'.'+type(model).__name__
|
| 62 |
+
assert type(model).__module__.startswith('transformers_modules.')
|
| 63 |
+
return model.eval()
|
| 64 |
+
model=step('model',load)
|
| 65 |
+
def weights():
|
| 66 |
+
from safetensors import safe_open
|
| 67 |
+
missing=[]
|
| 68 |
+
with safe_open(str(MODEL/'model.safetensors'),framework='pt') as f:
|
| 69 |
+
for name,param in model.named_parameters():
|
| 70 |
+
if not torch.equal(param,f.get_tensor(name).to(param.dtype)):missing.append(name)
|
| 71 |
+
report['parameter_mismatches']=missing;assert not missing,missing
|
| 72 |
+
for module in model.modules():
|
| 73 |
+
if type(module).__name__=='RotaryEmbedding':
|
| 74 |
+
expected,_=module.compute_default_rope_parameters(module.config)
|
| 75 |
+
assert torch.equal(expected,module.inv_freq)
|
| 76 |
+
elif type(module).__name__=='SigLIPRotaryEmbedding':
|
| 77 |
+
expected=1/(module.theta**(torch.arange(0,module.dim,2,dtype=torch.float32)/module.dim))
|
| 78 |
+
assert torch.equal(expected,module.inv_freq)
|
| 79 |
+
step('weights_and_buffers',weights)
|
| 80 |
+
def mask_semantics():
|
| 81 |
+
import sys
|
| 82 |
+
create=sys.modules[type(model).__module__]._create_causal_mask
|
| 83 |
+
from types import SimpleNamespace
|
| 84 |
+
c=SimpleNamespace(_attn_implementation='eager')
|
| 85 |
+
x=torch.zeros(1,3,4)
|
| 86 |
+
mask=create(c,x,torch.ones(1,3),None,torch.arange(3))
|
| 87 |
+
assert torch.equal(mask[0,0]==0,torch.ones(3,3,dtype=torch.bool).tril())
|
| 88 |
+
cache=SimpleNamespace(get_seq_length=lambda:5,is_compileable=False)
|
| 89 |
+
mask=create(c,torch.zeros(1,1,4),torch.tensor([[0,0,1,1,1,1]]),cache,torch.tensor([5]))
|
| 90 |
+
assert torch.equal(mask[0,0,0]==0,torch.tensor([False,False,True,True,True,True]))
|
| 91 |
+
allowed=torch.tensor([[[[True,False],[True,True]]]])
|
| 92 |
+
mask=create(c,torch.zeros(1,2,4),allowed,None,torch.arange(2))
|
| 93 |
+
assert torch.equal(mask==0,allowed)
|
| 94 |
+
step('mask_semantics',mask_semantics)
|
| 95 |
+
processor.tokenizer.padding_side='left'
|
| 96 |
+
def inputs(names,prompt='OCR:'):
|
| 97 |
+
images=[];messages=[]
|
| 98 |
+
for name in names:
|
| 99 |
+
with Image.open(INPUTS/name) as f:image=f.convert('RGB')
|
| 100 |
+
images.append(image)
|
| 101 |
+
messages.append([{'role':'user','content':[{'type':'image','image':image},{'type':'text','text':prompt}]}])
|
| 102 |
+
texts=processor.apply_chat_template(messages,tokenize=False,add_generation_prompt=True)
|
| 103 |
+
return processor(text=texts,images=images,padding=True,return_tensors='pt')
|
| 104 |
+
def generate(names,prompt='OCR:',max_tokens=128,use_cache=True):
|
| 105 |
+
batch=inputs(names,prompt)
|
| 106 |
+
with torch.inference_mode(): out=model.generate(**batch,max_new_tokens=max_tokens,do_sample=False,use_cache=use_cache)
|
| 107 |
+
ids=out[:,batch.input_ids.shape[1]:]
|
| 108 |
+
texts=processor.batch_decode(ids,skip_special_tokens=True,clean_up_tokenization_spaces=False)
|
| 109 |
+
return ids.tolist(),texts
|
| 110 |
+
def english():
|
| 111 |
+
ids,texts=generate(['invoice_en.png'])
|
| 112 |
+
report['english']={'ids':ids,'text':texts}
|
| 113 |
+
assert texts[0]=='PaddleOCR-VL 1.6\nInvoice No: INV-2026-1022\nTotal: USD 128.50',texts
|
| 114 |
+
assert 2 in ids[0]
|
| 115 |
+
step('english_generate',english)
|
| 116 |
+
def cache():
|
| 117 |
+
ids,text=generate(['invoice_en.png'],max_tokens=2,use_cache=False)
|
| 118 |
+
assert ids[0]==report['english']['ids'][0][:2],(ids,text)
|
| 119 |
+
step('uncached_prefix',cache)
|
| 120 |
+
def batch_logits():
|
| 121 |
+
individual=[]
|
| 122 |
+
for name in ['invoice_en.png','table_en.png']:
|
| 123 |
+
batch=inputs([name])
|
| 124 |
+
model.rope_deltas=None
|
| 125 |
+
with torch.inference_mode(): individual.append(model(**batch,use_cache=False).logits[:,-1].clone())
|
| 126 |
+
batch=inputs(['invoice_en.png','table_en.png'])
|
| 127 |
+
model.rope_deltas=None
|
| 128 |
+
with torch.inference_mode(): together=model(**batch,use_cache=False).logits[:,-1]
|
| 129 |
+
separate=torch.cat(individual)
|
| 130 |
+
maximum=(together-separate).abs().max().item()
|
| 131 |
+
report['batch_logits']={'max_abs_difference':maximum,'individual_argmax':separate.argmax(-1).tolist(),'batch_argmax':together.argmax(-1).tolist()}
|
| 132 |
+
assert torch.equal(together.argmax(-1),separate.argmax(-1))
|
| 133 |
+
torch.testing.assert_close(together,separate,atol=2e-3,rtol=2e-3)
|
| 134 |
+
step('batch_prefill_isolation',batch_logits)
|
| 135 |
+
if args.full:
|
| 136 |
+
def uncached_full():
|
| 137 |
+
ids,text=generate(['invoice_en.png'],use_cache=False)
|
| 138 |
+
report['uncached_english']={'ids':ids,'text':text}
|
| 139 |
+
assert ids==report['english']['ids']
|
| 140 |
+
step('uncached_full_english',uncached_full)
|
| 141 |
+
def full():
|
| 142 |
+
reference=[]
|
| 143 |
+
for name in ['invoice_en.png','paddle_general_ocr_001.png']:
|
| 144 |
+
_,text=generate([name]);reference.append(text[0])
|
| 145 |
+
ids,text=generate(['invoice_en.png','paddle_general_ocr_001.png'])
|
| 146 |
+
report['full_batch']={'individual':reference,'batch':text,'ids':ids}
|
| 147 |
+
assert text==reference,(text,reference)
|
| 148 |
+
assert all(2 in row for row in ids)
|
| 149 |
+
_,table=generate(['table_en.png'],'Table Recognition:')
|
| 150 |
+
report['table']=table
|
| 151 |
+
assert all(part in table[0] for part in ['Item','Qty','Price','Apple','10.00','Orange','12.50'])
|
| 152 |
+
step('full_chinese_batch_table',full)
|
| 153 |
+
report['passed']=True
|
| 154 |
+
except Exception:
|
| 155 |
+
report['passed']=False
|
| 156 |
+
finally:
|
| 157 |
+
save()
|
| 158 |
+
print(VERSION,report['passed'],flush=True)
|
| 159 |
+
raise SystemExit(0 if report['passed'] else 1)
|