MartialTerran commited on
Commit
59d74cf
·
verified ·
1 Parent(s): 360a3b3

Delete Surgical_Vocab_Reduction_for_QWEN2B_DotOCRv1.5_V1.0untested.py

Browse files
Surgical_Vocab_Reduction_for_QWEN2B_DotOCRv1.5_V1.0untested.py DELETED
@@ -1,482 +0,0 @@
1
- print("starting Surgical_Vocab_Reduction_for_QWEN2B_DotOCRv1.5_V1.0untested.py")
2
- r"""
3
- surgical_vocab_reduction.py
4
-
5
- Description:
6
- Surgically prunes the text vocabulary of Qwen2/dots.ocr-1.5 based models.
7
- Removes Chinese, Cyrillic, Arabic, and other non-European characters from
8
- the tokenizer, while preserving English, French, Spanish, special vision
9
- tokens, layout coordinate symbols, and byte fallback sequences.
10
-
11
- Dynamically prunes model weight files (safetensors/bin) to adjust
12
- the input embeddings and output LM head dimensions to the new vocabulary size,
13
- updating config.json and generation configs.
14
-
15
- Dependencies:
16
- - torch
17
- - safetensors
18
- - transformers
19
-
20
- Surgical Vocabulary & Parameter Reduction for Qwen2 / dots.ocr-1.5 (V1.0 Untested)
21
-
22
- This repository contains details and scripts for
23
- Surgical_Vocab_Reduction_for_QWEN2B_DotOCRv1.5_V1.0untested.py, a pipeline
24
- designed to prune the large multilingual vocabulary of the dots.ocr-1.5 model
25
- (and other Qwen2-based Vision-Language Models).
26
-
27
- By surgically removing non-target Unicode blocks (such as Chinese, Cyrillic,
28
- Arabic, Hebrew, Japanese, and Korean) while keeping English, French, Spanish,
29
- special layout coordinate symbols, and byte-fallback sequences, this script
30
- significantly reduces both the model's disk/VRAM footprint and its output logit
31
- calculation latency during local edge inference (such as on Intel Core Ultra
32
- NPUs or mobile devices).
33
-
34
- This implementation is inspired by and built upon the methodology outlined in
35
- Method for Dynamically Reducing Logit Computation in LLMs.
36
-
37
- 1. The Core Problem
38
-
39
- Modern, state-of-the-art multimodal models like dots.ocr-1.5 are designed for
40
- global multilingual coverage. To achieve this, they utilize very large
41
- vocabularies (Qwen2 series, for example, features a vocabulary of 151,936
42
- tokens).
43
-
44
- While this makes the model highly capable globally, it introduces significant
45
- overhead for deployment scenarios limited to Western European scripts (e.g.,
46
- English, French, Spanish):
47
-
48
- 1. Parameter Inflation: The input embedding layer (model.embed_tokens.weight)
49
- and the final Language Modeling head (lm_head.weight) scale linearly with
50
- vocabulary size. At 151,936 tokens, these two layers alone consume hundreds
51
- of millions of parameters, adding several gigabytes of unnecessary storage
52
- and VRAM.
53
- 2. Computational Overhead (Logit Latency): During autoregressive token
54
- generation, the final layer performs a matrix multiplication of shape
55
- [batch_size * seq_len, hidden_dim] @ [hidden_dim, vocab_size] to compute
56
- logits. Performing this massive projection on every decoded token introduces
57
- latency, particularly on resource-constrained CPUs and edge NPUs.
58
-
59
- 2. The Surgical Reduction Method
60
-
61
- The script executes a targeted, multi-stage compression process that shrinks the
62
- model's text processing layers without degrading its fundamental visual-spatial,
63
- OCR, or layout parsing intelligence:
64
-
65
- [ Original VLM ] ──> [ 1. Scan Tokenizer & Filter Vocab ] ──> [ 2. Filter BPE Merges ]
66
- │ (ASCII, Latin-1, & Special Tags Only) │
67
- ▼ ▼
68
- [ Pruned Weights ] <── [ 4. Prune Embedding/Head Tensors ] <── [ 3. Re-index IDs ]
69
-
70
- Stage A: Tokenizer Parsing & Robust Filtering
71
-
72
- The script reads tokenizer.json and evaluates each token string against strict,
73
- multi-tier criteria to determine which to keep:
74
-
75
- - Preserve Special & Coordinate Tokens: Bounding box markers, segment
76
- boundaries, and visual layout tags (e.g., <|image_1|>, <|box_start|>, etc.)
77
- are detected and kept to protect the spatial parsing mechanism.
78
- - Preserve Byte-Fallback Mappings: To prevent the tokenizer from failing when
79
- encountering out-of-vocabulary characters, any token representing a single
80
- byte (Latin-1 range 0x00 - 0xFF or length-1 strings) is retained.
81
- - Targeted Script Deletion: Multi-character tokens containing Chinese
82
- characters, Japanese Kana, Hangul, Cyrillic, Arabic, Devanagari, or Thai are
83
- discarded.
84
- - Targeted Language Retention: Standard ASCII punctuation, numbers, and the
85
- accented character sets of English, French, and Spanish (using Latin-1 and
86
- Latin Extended-A Unicode ranges) are preserved.
87
-
88
- Stage B: Re-Indexing & Merge List Sanitization
89
-
90
- - Discarded tokens are stripped, and the remaining tokens are sequentially
91
- re-indexed from 0 to N_{\text{new}} - 1.
92
- - The Byte Pair Encoding (BPE) "merges" rules are filtered. If a merge pair
93
- contains a sub-token or output token that was deleted, the merge rule is
94
- safely omitted. This prevents tokenizer errors or lookup warnings.
95
-
96
- Stage C: Surgical Tensor Pruning
97
-
98
- - The script scans the model directory for weights files (both PyTorch .bin
99
- and Safetensors .safetensors formats).
100
- - It locates the input embedding weights (model.embed_tokens.weight or
101
- transformer.wte.weight) and the output projection head weights
102
- (lm_head.weight).
103
- - It extracts only the slices of these matrices corresponding to the indices
104
- of the retained vocabulary, updating their shapes dynamically.
105
-
106
- Stage D: Configuration Re-alignment
107
-
108
- - Updates config.json with the new "vocab_size".
109
- - Remaps bos_token_id, eos_token_id, and pad_token_id to their new values
110
- inside generation_config.json.
111
-
112
- 3. Anticipated Benefits
113
-
114
- - VRAM and Storage Compression: Shifting from 151,936 tokens down to an
115
- estimated 30,000 \text{--} 40,000 tokens significantly shrinks the
116
- parameters of the text layers. For a hidden size of D = 3584, this reduction
117
- strips away over 800 million parameters, translating to immediate savings of
118
- up to 1.6 GB of VRAM/storage in FP16 (or comparable savings under quantized
119
- configurations).
120
- - Improved Generation Latency: Computing output logits against a vocabulary
121
- that is roughly 75\% smaller reduces the arithmetic intensity of the final
122
- lm_head projection layer by up to 75\%, improving token-generation speed on
123
- resource-constrained CPUs and edge NPUs.
124
- - Codebase & Edge Independence: By outputting standard, structurally valid
125
- Hugging Face files, the resulting compressed model can be loaded natively by
126
- downstream optimization toolkits (such as OpenVINO or ONNX Runtime) without
127
- requiring custom code layers.
128
-
129
- 4. How to Use
130
-
131
- Setup & Dependencies
132
-
133
- Ensure your python environment has the required libraries installed:
134
-
135
- pip install torch safetensors transformers
136
-
137
- Directory Structure
138
-
139
- Place your original downloaded weights for dots.ocr-1.5 in a source directory,
140
- and designate a destination directory for the pruned output. Example:
141
-
142
- ./weights/
143
- ├── dots_ocr_1_5/ <-- Original weights directory
144
- │ ├── config.json
145
- │ ├── tokenizer.json
146
- │ ├── model-00001-of-00002.safetensors
147
- │ └── ...
148
- └── dots_ocr_1_5_pruned/ <-- Target destination for pruned output
149
-
150
- Execution
151
-
152
- Run the script from your terminal:
153
-
154
- python Surgical_Vocab_Reduction_for_QWEN2B_DotOCRv1.5_V1.0untested.py
155
-
156
- Upon successful execution, the script will output the pruned weights, tokenizer
157
- structures, and updated configurations directly into your target directory. You
158
- can then load, quantize (e.g., using OpenVINO for Intel NPUs), or test the
159
- compressed model.
160
-
161
- 5. Disclaimer & Untested Notice
162
-
163
- Please note that this script is marked as V1.0 Untested.
164
-
165
- While designed with strong safeguards—such as preserving all special layout
166
- coordinates, maintaining the byte-fallback system, and pruning the BPE merge
167
- sequences—empirical validation is required.
168
-
169
- Users are encouraged to run evaluations and verification benchmarks (such as
170
- measuring layout extraction accuracy, tokenization rate, and logit output
171
- accuracy) on their pruned models to confirm that model performance meets
172
- expectations for their targeted language profiles.
173
-
174
- """
175
-
176
- import os
177
- import re
178
- import json
179
- import glob
180
- import shutil
181
- import torch
182
- from pathlib import Path
183
-
184
- # --- Configuration ---
185
- MODEL_DIR = r"./weights/dots_ocr_1_5" # Source model folder
186
- OUTPUT_DIR = r"./weights/dots_ocr_1_5_pruned" # Pruned output destination
187
-
188
- # Unwanted Unicode blocks to target for deletion
189
- UNWANTED_BLOCKS = [
190
- (0x4E00, 0x9FFF), # CJK Unified Ideographs (Chinese)
191
- (0x3400, 0x4DBF), # CJK Unified Ideographs Extension A
192
- (0x0400, 0x04FF), # Cyrillic
193
- (0x0600, 0x06FF), # Arabic
194
- (0xAC00, 0xD7AF), # Hangul Syllables (Korean)
195
- (0x3040, 0x30FF), # Hiragana & Katakana (Japanese)
196
- (0x1100, 0x11FF), # Hangul Jamo
197
- (0x0590, 0x05FF), # Hebrew
198
- (0x0900, 0x097F), # Devanagari (Hindi)
199
- (0x0A00, 0x0A7F), # Gurmukhi
200
- (0x0B00, 0x0B7F), # Oriya
201
- (0x0E00, 0x0E7F), # Thai
202
- ]
203
-
204
- # Keys used for embeddings in Hugging Face models
205
- EMBED_KEYS = [
206
- "model.embed_tokens.weight", # Llama/Qwen standard input embeddings
207
- "transformer.wte.weight", # Alternate GPT/Qwen variant keys
208
- ]
209
- LM_HEAD_KEYS = [
210
- "lm_head.weight", # Output LM Head mapping
211
- ]
212
-
213
- def should_keep_token(token_str: str) -> bool:
214
- """
215
- Evaluates whether a token should be retained based on character validation rules.
216
- """
217
- # 1. ALWAYS preserve special system & layout coordinate tokens
218
- if token_str.startswith("<|") and token_str.endswith("|>"):
219
- return True
220
-
221
- # 2. ALWAYS keep single byte/single character tokens to prevent breaking byte fallbacks
222
- if len(token_str) == 1:
223
- return True
224
-
225
- # 3. ALWAYS preserve raw byte fallbacks (e.g., <0xAF>)
226
- if re.match(r"^<0x[0-9A-Fa-f]{2}>$", token_str):
227
- return True
228
-
229
- # 4. Check characters against unwanted Unicode script ranges
230
- for char in token_str:
231
- codepoint = ord(char)
232
- for start, end in UNWANTED_BLOCKS:
233
- if start <= codepoint <= end:
234
- return False
235
-
236
- return True
237
-
238
- def prune_tokenizer_json(src_path: Path, dest_path: Path):
239
- """
240
- Parses, filters, re-indexes, and saves the HF fast tokenizer configurations.
241
- """
242
- print(f"Reading tokenizer config: {src_path.name}")
243
- with open(src_path, "r", encoding="utf-8") as f:
244
- data = json.load(f)
245
-
246
- model_data = data.get("model", {})
247
- if not model_data or model_data.get("type") != "BPE":
248
- print("[Warning] Tokenizer is not BPE. Skipping structural modifications.")
249
- return None, None
250
-
251
- vocab = model_data.get("vocab", {})
252
- merges = model_data.get("merges", [])
253
-
254
- # Filter the vocabulary
255
- kept_vocab = {}
256
- kept_indices = []
257
-
258
- # Determine which tokens to retain
259
- for token_str, token_id in vocab.items():
260
- if should_keep_token(token_str):
261
- kept_vocab[token_str] = token_id
262
- kept_indices.append(token_id)
263
-
264
- # Re-index remaining tokens sequentially
265
- kept_indices.sort()
266
- old_to_new_id = {old_id: new_idx for new_idx, old_id in enumerate(kept_indices)}
267
-
268
- new_vocab = {token_str: old_to_new_id[old_id] for token_str, old_id in kept_vocab.items()}
269
-
270
- # Filter merges to avoid referencing deleted BPE components
271
- new_merges = []
272
- for merge_rule in merges:
273
- parts = merge_rule.split(" ")
274
- if len(parts) == 2:
275
- left, right = parts[0], parts[1]
276
- joined = left + right
277
- # If sub-components are present in the vocab, retain the merge
278
- if left in new_vocab and right in new_vocab and joined in new_vocab:
279
- new_merges.append(merge_rule)
280
-
281
- # Update Tokenizer structural dictionaries
282
- data["model"]["vocab"] = new_vocab
283
- data["model"]["merges"] = new_merges
284
-
285
- # Re-index "added_tokens"
286
- if "added_tokens" in data:
287
- new_added = []
288
- for token in data["added_tokens"]:
289
- old_id = token.get("id")
290
- if old_id in old_to_new_id:
291
- token["id"] = old_to_new_id[old_id]
292
- new_added.append(token)
293
- data["added_tokens"] = new_added
294
-
295
- # Save the modified JSON configuration
296
- with open(dest_path, "w", encoding="utf-8") as f:
297
- json.dump(data, f, indent=2, ensure_ascii=False)
298
-
299
- print(f"Pruned tokenizer vocabulary: {len(vocab)} -> {len(new_vocab)} tokens.")
300
- return kept_indices, old_to_new_id
301
-
302
- def prune_legacy_vocab_files(src_dir: Path, dest_dir: Path, old_to_new_id: dict):
303
- """
304
- Prunes companion files like vocab.json or merges.json if present.
305
- """
306
- vocab_json_path = src_dir / "vocab.json"
307
- if vocab_json_path.exists():
308
- with open(vocab_json_path, "r", encoding="utf-8") as f:
309
- vocab = json.load(f)
310
- new_vocab = {}
311
- for token, old_id in vocab.items():
312
- if old_id in old_to_new_id:
313
- new_vocab[token] = old_to_new_id[old_id]
314
- with open(dest_dir / "vocab.json", "w", encoding="utf-8") as f:
315
- json.dump(new_vocab, f, indent=2, ensure_ascii=False)
316
-
317
- merges_json_path = src_dir / "merges.json"
318
- if merges_json_path.exists():
319
- with open(merges_json_path, "r", encoding="utf-8") as f:
320
- merges = json.load(f)
321
- new_merges = []
322
- for rule in merges:
323
- parts = rule.split(" ")
324
- if len(parts) == 2 and parts[0] in new_vocab and parts[1] in new_vocab:
325
- new_merges.append(rule)
326
- with open(dest_dir / "merges.json", "w", encoding="utf-8") as f:
327
- json.dump(new_merges, f, indent=2, ensure_ascii=False)
328
-
329
- def update_model_configs(src_dir: Path, dest_dir: Path, new_vocab_size: int, old_to_new_id: dict):
330
- """
331
- Updates configuration variables and targets sequence tokens inside config and generation metadata files.
332
- """
333
- config_path = src_dir / "config.json"
334
- if config_path.exists():
335
- with open(config_path, "r", encoding="utf-8") as f:
336
- config = json.load(f)
337
- config["vocab_size"] = new_vocab_size
338
- with open(dest_dir / "config.json", "w", encoding="utf-8") as f:
339
- json.dump(config, f, indent=2)
340
-
341
- # Remap base configuration targets inside Generation configuration lists
342
- gen_config_path = src_dir / "generation_config.json"
343
- if gen_config_path.exists():
344
- with open(gen_config_path, "r", encoding="utf-8") as f:
345
- gconfig = json.load(f)
346
- for key in ["bos_token_id", "eos_token_id", "pad_token_id"]:
347
- if key in gconfig:
348
- val = gconfig[key]
349
- if isinstance(val, int) and val in old_to_new_id:
350
- gconfig[key] = old_to_new_id[val]
351
- elif isinstance(val, list):
352
- gconfig[key] = [old_to_new_id[v] for v in val if v in old_to_new_id]
353
- with open(dest_dir / "generation_config.json", "w", encoding="utf-8") as f:
354
- json.dump(gconfig, f, indent=2)
355
-
356
- def prune_safetensors_weights(src_dir: Path, dest_dir: Path, kept_indices: list):
357
- """
358
- Loads Safetensors parameters dynamically, prunes target spatial dimensions,
359
- and writes optimized bin components.
360
- """
361
- from safetensors import safe_open
362
- from safetensors.torch import save_file
363
-
364
- indices_tensor = torch.tensor(kept_indices, dtype=torch.long)
365
- safetensor_files = glob.glob(str(src_dir / "*.safetensors"))
366
-
367
- for file_path in safetensor_files:
368
- p_path = Path(file_path)
369
- print(f"Processing weights file: {p_path.name}")
370
-
371
- tensors = {}
372
- modified = False
373
-
374
- with safe_open(file_path, framework="pt", device="cpu") as f:
375
- for k in f.keys():
376
- tensor_val = f.get_tensor(k)
377
-
378
- # Check for input or output embedding weights
379
- if k in EMBED_KEYS or k in LM_HEAD_KEYS:
380
- print(f" -> Pruning target embedding layer: '{k}'")
381
- print(f" Original dimensions: {list(tensor_val.shape)}")
382
- tensor_val = tensor_val[indices_tensor]
383
- print(f" New dimensions: {list(tensor_val.shape)}")
384
- modified = True
385
-
386
- tensors[k] = tensor_val
387
-
388
- # Save output
389
- out_file = dest_dir / p_path.name
390
- save_file(tensors, str(out_file))
391
-
392
- return modified
393
-
394
- def prune_pytorch_bin_weights(src_dir: Path, dest_dir: Path, kept_indices: list):
395
- """
396
- Alternative handler for models using legacy PyTorch binary format weights files.
397
- """
398
- indices_tensor = torch.tensor(kept_indices, dtype=torch.long)
399
- bin_files = glob.glob(str(src_dir / "*.bin"))
400
-
401
- for file_path in bin_files:
402
- p_path = Path(file_path)
403
- print(f"Processing weights file: {p_path.name}")
404
-
405
- state_dict = torch.load(file_path, map_location="cpu")
406
- modified = False
407
-
408
- for k in list(state_dict.keys()):
409
- if k in EMBED_KEYS or k in LM_HEAD_KEYS:
410
- print(f" -> Pruning target embedding layer: '{k}'")
411
- print(f" Original dimensions: {list(state_dict[k].shape)}")
412
- state_dict[k] = state_dict[k][indices_tensor]
413
- print(f" New dimensions: {list(state_dict[k].shape)}")
414
- modified = True
415
-
416
- torch.save(state_dict, dest_dir / p_path.name)
417
-
418
- return modified
419
-
420
- def copy_miscellaneous_files(src_dir: Path, dest_dir: Path):
421
- """
422
- Copies companion weights files, structural classes, and configuration wrappers.
423
- """
424
- extensions = ["*.py", "*.png", "*.txt", "*.md", "preprocessor_config.json", "processor_config.json"]
425
- for ext in extensions:
426
- for f in glob.glob(str(src_dir / ext)):
427
- shutil.copy(f, dest_dir)
428
-
429
- def main():
430
- print("=" * 80)
431
- print(" Surgical Vocab and Weight Parameter Reducer")
432
- print("=" * 80)
433
-
434
- src_dir = Path(MODEL_DIR)
435
- dest_dir = Path(OUTPUT_DIR)
436
-
437
- if not src_dir.exists():
438
- print(f"[Error] Source model folder path does not exist: {src_dir}")
439
- return
440
-
441
- dest_dir.mkdir(parents=True, exist_ok=True)
442
-
443
- # 1. Prune the tokenizer vocabulary mapping
444
- tokenizer_json = src_dir / "tokenizer.json"
445
- if not tokenizer_json.exists():
446
- print(f"[Error] 'tokenizer.json' was not found in: {src_dir}")
447
- return
448
-
449
- kept_indices, old_to_new_id = prune_tokenizer_json(tokenizer_json, dest_dir / "tokenizer.json")
450
- if kept_indices is None:
451
- print("[Error] Failed to prune tokenizer.")
452
- return
453
-
454
- new_vocab_size = len(kept_indices)
455
-
456
- # 2. Prune alternative/legacy files if present
457
- prune_legacy_vocab_files(src_dir, dest_dir, old_to_new_id)
458
-
459
- # 3. Process structural weights
460
- print("\nStarting weight parameter pruning...")
461
- has_safetensors = len(glob.glob(str(src_dir / "*.safetensors"))) > 0
462
-
463
- if has_safetensors:
464
- prune_safetensors_weights(src_dir, dest_dir, kept_indices)
465
- else:
466
- prune_pytorch_bin_weights(src_dir, dest_dir, kept_indices)
467
-
468
- # 4. Save metadata settings adjustments
469
- update_model_configs(src_dir, dest_dir, new_vocab_size, old_to_new_id)
470
-
471
- # 5. Bring over vision projections, license texts, and system processors
472
- copy_miscellaneous_files(src_dir, dest_dir)
473
-
474
- print("\n" + "=" * 80)
475
- print("Reduction pipeline completed successfully.")
476
- print(f" - Original Vocab Size (approx): {151936 if '1.5' in MODEL_DIR else 'Unknown'}")
477
- print(f" - New Pruned Vocab Size: {new_vocab_size}")
478
- print(f" - Reduced Weight Files Saved: {dest_dir}")
479
- print("=" * 80)
480
-
481
- if __name__ == "__main__":
482
- main()