Bvicii commited on
Commit
057f27f
·
verified ·
1 Parent(s): 7fa00a8

Fix remote-code compatibility across Transformers 4.x and 5.x

Browse files

This 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 CHANGED
@@ -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.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
 
 
 
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 = create_causal_mask(
607
- config=self.config,
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
- attn_output, attn_weights = attention_interface(
1138
- self,
1139
- queries,
1140
- keys,
1141
- values,
1142
- attention_mask,
1143
- is_causal=self.is_causal,
1144
- scaling=self.scale,
1145
- dropout=0.0 if not self.training else self.dropout,
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.data)
1296
- nn.init.xavier_uniform_(module.attention.in_proj_weight.data)
1297
- nn.init.zeros_(module.attention.in_proj_bias.data)
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
- module.bias.data.zero_()
1304
- module.weight.data.fill_(1.0)
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
- if cache_position[0] != 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
tests/README.md ADDED
@@ -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.
tests/compatibility_results.json ADDED
@@ -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
+ }
tests/data/invoice_en.png ADDED
tests/data/paddle_general_ocr_001.png ADDED
tests/data/table_en.png ADDED
tests/test_remote_code_compat.py ADDED
@@ -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)