acul3 commited on
Commit
02acc80
·
verified ·
1 Parent(s): f7962bb

Upload scripts/test_e2e_v2.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/test_e2e_v2.py +352 -0
scripts/test_e2e_v2.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ E2E Validation v2: Fixed resolution pipeline.
4
+ Forces all images to 1120x1540 so token count matches fixed vision encoder output.
5
+ Compares HF model output vs fixed PyTorch modules output.
6
+ """
7
+
8
+ import os, sys, time, torch, torch.nn.functional as F
9
+ from pathlib import Path
10
+ from PIL import Image
11
+
12
+ # ── Constants ──
13
+ MODEL_DIR = "./models/LightOnOCR-2-1B"
14
+ FIXED_H, FIXED_W = 1120, 1540
15
+ PATCH_SIZE = 14
16
+ SPATIAL_MERGE = 2
17
+ MERGED_H = FIXED_H // PATCH_SIZE // SPATIAL_MERGE # 40
18
+ MERGED_W = FIXED_W // PATCH_SIZE // SPATIAL_MERGE # 55
19
+ NUM_IMG_TOKENS = MERGED_H * MERGED_W # 2200
20
+ NUM_VPAD_TOKENS = MERGED_H - 1 # 39
21
+
22
+ # Token IDs
23
+ IMAGE_TOKEN_ID = 151655
24
+ VISION_PAD_ID = 151654
25
+ VISION_END_ID = 151653
26
+ IM_START_ID = 151644
27
+ IM_END_ID = 151645
28
+ EOS_TOKEN_ID = 151645
29
+
30
+ # Decoder constants
31
+ NUM_LAYERS = 28
32
+ NUM_KV_HEADS = 8
33
+ NUM_HEADS = 16
34
+ HEAD_DIM = 128
35
+ HIDDEN_SIZE = 1024
36
+ MAX_SEQ_LEN = 4096 # Large enough for 2256 input + generation
37
+
38
+
39
+ def download_test_images():
40
+ """Get real test images."""
41
+ import requests
42
+ os.makedirs("test_images", exist_ok=True)
43
+ images = {}
44
+
45
+ sources = {
46
+ "receipt": "https://huggingface.co/datasets/hf-internal-testing/fixtures_ocr/resolve/main/SROIE-receipt.jpeg",
47
+ }
48
+ for name, url in sources.items():
49
+ path = f"test_images/{name}.png"
50
+ if not os.path.exists(path):
51
+ print(f" Downloading {name}...")
52
+ try:
53
+ resp = requests.get(url, timeout=30)
54
+ resp.raise_for_status()
55
+ with open(path, "wb") as f:
56
+ f.write(resp.content)
57
+ except Exception as e:
58
+ print(f" FAILED: {e}")
59
+ continue
60
+ img = Image.open(path).convert("RGB")
61
+ images[name] = img
62
+ print(f" {name}: {img.size}")
63
+
64
+ # Synthetic doc
65
+ img = Image.new("RGB", (800, 600), "white")
66
+ from PIL import ImageDraw
67
+ draw = ImageDraw.Draw(img)
68
+ draw.text((50, 50), "Invoice #12345", fill="black")
69
+ draw.text((50, 100), "Date: 2024-01-15", fill="black")
70
+ draw.text((50, 150), "Item 1: Widget x5 @ $10.00 = $50.00", fill="black")
71
+ draw.text((50, 200), "Item 2: Gadget x2 @ $24.99 = $49.98", fill="black")
72
+ draw.text((50, 250), "Total: $99.98", fill="black")
73
+ img.save("test_images/synthetic.png")
74
+ images["synthetic"] = img
75
+ print(f" synthetic: {img.size}")
76
+
77
+ return images
78
+
79
+
80
+ def build_fixed_input_ids(processor, text_prompt="OCR this document. Extract all text."):
81
+ """
82
+ Build input_ids with exactly NUM_IMG_TOKENS image tokens,
83
+ matching our fixed 1120x1540 vision encoder output.
84
+ Uses the processor's chat template but with a fixed-size image.
85
+ """
86
+ # Create a dummy image at exact target size to get correct token count
87
+ dummy_img = Image.new("RGB", (FIXED_W, FIXED_H), "white")
88
+ messages = [{"role": "user", "content": [
89
+ {"type": "image"}, {"type": "text", "text": text_prompt}
90
+ ]}]
91
+ text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
92
+ inputs = processor(text=text, images=[dummy_img], return_tensors="pt")
93
+
94
+ input_ids = inputs["input_ids"]
95
+ # Verify token counts
96
+ ids_list = input_ids[0].tolist()
97
+ n_img = ids_list.count(IMAGE_TOKEN_ID)
98
+ n_pad = ids_list.count(VISION_PAD_ID)
99
+ assert n_img == NUM_IMG_TOKENS, f"Expected {NUM_IMG_TOKENS} IMG tokens, got {n_img}"
100
+ print(f" Input template: {input_ids.shape[1]} tokens ({n_img} IMG, {n_pad} VPAD)")
101
+ return input_ids
102
+
103
+
104
+ def preprocess_image_fixed(img, processor):
105
+ """Resize image to exactly FIXED_H x FIXED_W and get pixel_values."""
106
+ # Resize maintaining content, pad to target
107
+ img_resized = img.resize((FIXED_W, FIXED_H), Image.LANCZOS)
108
+ # Use processor's image normalization
109
+ dummy_messages = [{"role": "user", "content": [{"type": "image"}]}]
110
+ text = processor.apply_chat_template(dummy_messages, add_generation_prompt=True, tokenize=False)
111
+ inputs = processor(text=text, images=[img_resized], return_tensors="pt")
112
+ return inputs["pixel_values"]
113
+
114
+
115
+ def run_hf_model(images, processor):
116
+ """Run original HF model with FIXED resolution preprocessing."""
117
+ from transformers import AutoModelForImageTextToText
118
+ from safetensors.torch import load_file
119
+
120
+ print("\n[HF Model] Loading...")
121
+ model = AutoModelForImageTextToText.from_pretrained(
122
+ MODEL_DIR, dtype=torch.bfloat16, attn_implementation="sdpa", device_map="cpu",
123
+ )
124
+ state_dict = load_file(os.path.join(MODEL_DIR, "model.safetensors"))
125
+ remapped = {k.replace("model.vision_encoder.", "model.vision_tower.")
126
+ .replace("model.vision_projection.", "model.multi_modal_projector."): v
127
+ for k, v in state_dict.items()}
128
+ model.load_state_dict(remapped, strict=False)
129
+ model = model.to("cuda").eval()
130
+
131
+ results = {}
132
+ for name, img in images.items():
133
+ print(f"\n [{name}] HF generate (fixed {FIXED_H}x{FIXED_W})...")
134
+ pixel_values = preprocess_image_fixed(img, processor).to("cuda")
135
+ input_ids = build_fixed_input_ids(processor).to("cuda")
136
+ attention_mask = torch.ones_like(input_ids)
137
+
138
+ input_len = input_ids.shape[1]
139
+ t0 = time.time()
140
+ with torch.no_grad():
141
+ output_ids = model.generate(
142
+ input_ids=input_ids,
143
+ pixel_values=pixel_values,
144
+ attention_mask=attention_mask,
145
+ image_sizes=torch.tensor([[FIXED_H, FIXED_W]], device="cuda"),
146
+ max_new_tokens=512, do_sample=False, temperature=None, top_p=None,
147
+ )
148
+ elapsed = time.time() - t0
149
+ new_ids = output_ids[0, input_len:]
150
+ text = processor.tokenizer.decode(new_ids, skip_special_tokens=True)
151
+ n_tok = len(new_ids)
152
+ print(f" {n_tok} tokens, {elapsed:.1f}s ({n_tok/elapsed:.1f} tok/s)")
153
+ print(f" Output: {text[:200]}...")
154
+ results[name] = {"text": text, "tokens": n_tok, "time": elapsed}
155
+
156
+ del model
157
+ torch.cuda.empty_cache()
158
+ return results
159
+
160
+
161
+ def run_fixed_modules(images, processor):
162
+ """Run fixed PyTorch modules E2E with proper token matching."""
163
+ sys.path.insert(0, ".")
164
+ from export_vision import build_vision_module, load_original_model
165
+ from export_decoder import TextDecoderFixed, build_decoder_module
166
+
167
+ print("\n[Fixed Modules] Loading...")
168
+ orig = load_original_model()
169
+
170
+ vision = build_vision_module(orig)
171
+ decoder = build_decoder_module(orig)
172
+ embed_tokens = orig.model.language_model.embed_tokens
173
+
174
+ device = "cuda"
175
+ dtype = torch.bfloat16
176
+ vision = vision.to(device).to(dtype).eval()
177
+ decoder = decoder.to(device).to(dtype).eval()
178
+ embed_tokens = embed_tokens.to(device).to(dtype)
179
+
180
+ del orig
181
+ torch.cuda.empty_cache()
182
+
183
+ results = {}
184
+ for name, img in images.items():
185
+ print(f"\n [{name}] Fixed modules E2E...")
186
+ try:
187
+ # Step 1: Get pixel values at fixed resolution
188
+ pixel_values = preprocess_image_fixed(img, processor).to(device).to(dtype)
189
+ input_ids = build_fixed_input_ids(processor).to(device)
190
+
191
+ # Step 2: Vision encoder
192
+ with torch.no_grad():
193
+ image_features = vision(pixel_values) # [1, 2200, 1024]
194
+ print(f" Vision output: {image_features.shape}")
195
+
196
+ # Step 3: Build combined embeddings
197
+ with torch.no_grad():
198
+ text_embeds = embed_tokens(input_ids) # [1, seq_len, 1024]
199
+
200
+ ids_list = input_ids[0].tolist()
201
+ img_positions = [i for i, t in enumerate(ids_list) if t == IMAGE_TOKEN_ID]
202
+ assert len(img_positions) == image_features.shape[1], \
203
+ f"Token/feature mismatch: {len(img_positions)} slots vs {image_features.shape[1]} features"
204
+
205
+ combined = text_embeds.clone()
206
+ # Scatter vision features into IMG token positions
207
+ indices = torch.tensor(img_positions, device=device)
208
+ combined[0, indices] = image_features[0]
209
+
210
+ seq_len = combined.shape[1]
211
+ print(f" Combined seq: {seq_len}, scattering {len(img_positions)} features")
212
+
213
+ # Step 4: Prefill — feed combined embeddings through decoder
214
+ kv_caches = []
215
+ for _ in range(NUM_LAYERS):
216
+ k = torch.zeros(1, NUM_KV_HEADS, MAX_SEQ_LEN, HEAD_DIM, dtype=dtype, device=device)
217
+ v = torch.zeros(1, NUM_KV_HEADS, MAX_SEQ_LEN, HEAD_DIM, dtype=dtype, device=device)
218
+ kv_caches.extend([k, v])
219
+
220
+ position_ids = torch.arange(seq_len, device=device).unsqueeze(0)
221
+ cache_position = torch.arange(seq_len, device=device)
222
+
223
+ # Causal mask for prefill
224
+ mask = torch.full((1, 1, seq_len, MAX_SEQ_LEN), float("-inf"), dtype=dtype, device=device)
225
+ for i in range(seq_len):
226
+ mask[0, 0, i, :i+1] = 0.0
227
+
228
+ # Monkey-patch embed_tokens for prefill (we already have embeddings)
229
+ orig_embed = decoder.embed_tokens
230
+ class PrefillEmbed(torch.nn.Module):
231
+ def __init__(self, embeds):
232
+ super().__init__()
233
+ self.e = embeds
234
+ def forward(self, x):
235
+ return self.e
236
+ decoder.embed_tokens = PrefillEmbed(combined)
237
+
238
+ t0 = time.time()
239
+ with torch.no_grad():
240
+ result = decoder(input_ids[:, :seq_len], mask, position_ids, cache_position, *kv_caches)
241
+
242
+ decoder.embed_tokens = orig_embed
243
+
244
+ logits = result[0]
245
+ kv_caches = list(result[1:])
246
+ next_token = logits[0, -1].argmax().item()
247
+
248
+ generated = [next_token]
249
+ cur_pos = seq_len
250
+
251
+ # Step 5: Autoregressive decode
252
+ for step in range(511):
253
+ if next_token == EOS_TOKEN_ID or cur_pos >= MAX_SEQ_LEN:
254
+ break
255
+
256
+ token_input = torch.tensor([[next_token]], device=device)
257
+ pos_ids = torch.tensor([[cur_pos]], device=device)
258
+ cache_pos = torch.tensor([cur_pos], device=device)
259
+
260
+ # Decode mask: attend to all positions up to cur_pos
261
+ dmask = torch.zeros(1, 1, 1, MAX_SEQ_LEN, dtype=dtype, device=device)
262
+ dmask[0, 0, 0, cur_pos+1:] = float("-inf")
263
+
264
+ with torch.no_grad():
265
+ result = decoder(token_input, dmask, pos_ids, cache_pos, *kv_caches)
266
+
267
+ logits = result[0]
268
+ kv_caches = list(result[1:])
269
+ next_token = logits[0, -1].argmax().item()
270
+ generated.append(next_token)
271
+ cur_pos += 1
272
+
273
+ elapsed = time.time() - t0
274
+ text = processor.tokenizer.decode(generated, skip_special_tokens=True)
275
+ n_tok = len(generated)
276
+ print(f" {n_tok} tokens, {elapsed:.1f}s ({n_tok/elapsed:.1f} tok/s)")
277
+ print(f" Output: {text[:200]}...")
278
+ results[name] = {"text": text, "tokens": n_tok, "time": elapsed}
279
+
280
+ except Exception as e:
281
+ import traceback
282
+ traceback.print_exc()
283
+ results[name] = {"text": f"ERROR: {e}", "tokens": 0, "time": 0}
284
+
285
+ return results
286
+
287
+
288
+ def levenshtein(s1, s2):
289
+ if len(s1) < len(s2): return levenshtein(s2, s1)
290
+ if len(s2) == 0: return len(s1)
291
+ prev = list(range(len(s2) + 1))
292
+ for i, c1 in enumerate(s1):
293
+ curr = [i + 1]
294
+ for j, c2 in enumerate(s2):
295
+ curr.append(min(prev[j+1]+1, curr[j]+1, prev[j]+(c1!=c2)))
296
+ prev = curr
297
+ return prev[-1]
298
+
299
+
300
+ def compare(hf_results, fx_results, images):
301
+ print("\n" + "="*70)
302
+ print("E2E COMPARISON (Fixed 1120x1540 resolution)")
303
+ print("="*70)
304
+
305
+ for name in images:
306
+ hf = hf_results[name]
307
+ fx = fx_results[name]
308
+ hf_t, fx_t = hf["text"], fx["text"]
309
+
310
+ exact = hf_t.strip() == fx_t.strip()
311
+ ed = levenshtein(hf_t, fx_t)
312
+ max_len = max(len(hf_t), len(fx_t), 1)
313
+ char_acc = 1.0 - ed / max_len
314
+
315
+ ref_words = set(hf_t.lower().split())
316
+ hyp_words = set(fx_t.lower().split())
317
+ union = ref_words | hyp_words
318
+ word_acc = len(ref_words & hyp_words) / len(union) if union else 1.0
319
+
320
+ print(f"\n{'─'*70}")
321
+ print(f" [{name}]")
322
+ print(f" HF ({hf['tokens']} tok): {hf_t[:250]}")
323
+ print(f" FIX ({fx['tokens']} tok): {fx_t[:250]}")
324
+ print(f" Exact: {'✅ YES' if exact else '❌ NO'}")
325
+ print(f" Edit dist: {ed}, Char acc: {char_acc:.4f}, Word acc: {word_acc:.4f}")
326
+
327
+ print("\n" + "="*70)
328
+
329
+
330
+ def main():
331
+ print("LightOnOCR-2-1B E2E Validation v2")
332
+ print(f"Fixed resolution: {FIXED_H}x{FIXED_W} → {NUM_IMG_TOKENS} vision features")
333
+ print(f"Device: cuda, Max seq: {MAX_SEQ_LEN}")
334
+ print("="*70)
335
+
336
+ images = download_test_images()
337
+
338
+ from transformers import AutoProcessor
339
+ processor = AutoProcessor.from_pretrained(MODEL_DIR)
340
+
341
+ hf_results = run_hf_model(images, processor)
342
+
343
+ # Free GPU for fixed modules
344
+ torch.cuda.empty_cache()
345
+
346
+ fx_results = run_fixed_modules(images, processor)
347
+
348
+ compare(hf_results, fx_results, images)
349
+
350
+
351
+ if __name__ == "__main__":
352
+ main()
Free AI Image Generator No sign-up. Instant results. Open Now