1+ import argparse
12import bz2
23import hashlib
34import json
45import logging
6+ import os
57import platform
68import re
79import sys
10+ import time
11+ import tracemalloc
812from pathlib import Path
913from typing import Any , Dict , List , Optional , Tuple , Union
1014
2428RE_WHITESPACE = re .compile (r"\s+" )
2529
2630
31+ # helpers: New helper to compute SHA256 and return raw bytes directly
32+ def compute_sha256_bytes (
33+ * ,
34+ data : Optional [Union [bytes , bytearray ]] = None ,
35+ file_path : Optional [Union [str , Path ]] = None ,
36+ ) -> bytes :
37+ """
38+ Compute SHA256 hash of a file OR raw bytes, returning raw bytes.
39+ This avoids the overhead of converting to a hex string and back.
40+ """
41+ if (data is None ) == (file_path is None ):
42+ raise ValueError ("Exactly one of 'data' or 'file_path' must be provided." )
43+
44+ sha256 = hashlib .sha256 ()
45+
46+ if data is not None :
47+ sha256 .update (data )
48+ return sha256 .digest ()
49+
50+ path = Path (file_path )
51+ with path .open ("rb" ) as f :
52+ while chunk := f .read (8192 ):
53+ sha256 .update (chunk )
54+
55+ return sha256 .digest ()
56+
57+
2758# Merkle Tree Chunk-Level Hashing for Large Files
2859def compute_merkle_root (
2960 file_path : Union [str , Path ], chunk_size : int = MERKLE_CHUNK_SIZE_BYTES
@@ -80,8 +111,8 @@ def generate_merkle_proof(
80111 # Build leaf level
81112 with path .open ("rb" ) as f :
82113 while chunk := f .read (chunk_size ):
83- leaf_hex = compute_sha256 (data = chunk )
84- leaves .append (bytes . fromhex ( leaf_hex ) )
114+ leaf_bytes = compute_sha256_bytes (data = chunk )
115+ leaves .append (leaf_bytes )
85116
86117 if not leaves :
87118 raise ValueError ("Cannot generate proof for empty file" )
@@ -107,8 +138,8 @@ def generate_merkle_proof(
107138 next_level = []
108139 for i in range (0 , len (leaves ), 2 ):
109140 combined = leaves [i ] + leaves [i + 1 ]
110- parent_hex = compute_sha256 (data = combined )
111- next_level .append (bytes . fromhex ( parent_hex ) )
141+ parent_bytes = compute_sha256_bytes (data = combined )
142+ next_level .append (parent_bytes )
112143
113144 index //= 2
114145 leaves = next_level
@@ -121,7 +152,7 @@ def verify_merkle_proof(chunk_bytes: bytes, proof, merkle_root: str) -> bool:
121152 Verify a Merkle proof for given chunk bytes.
122153 """
123154 try :
124- current_hash = bytes . fromhex ( compute_sha256 ( data = chunk_bytes ) )
155+ current_hash = compute_sha256_bytes ( data = chunk_bytes )
125156 expected_root = bytes .fromhex (merkle_root )
126157 except (TypeError , ValueError ):
127158 return False
@@ -152,8 +183,7 @@ def verify_merkle_proof(chunk_bytes: bytes, proof, merkle_root: str) -> bool:
152183 else :
153184 combined = current_hash + sibling
154185
155- parent_hex = compute_sha256 (data = combined )
156- current_hash = bytes .fromhex (parent_hex )
186+ current_hash = compute_sha256_bytes (data = combined )
157187
158188 return current_hash == expected_root
159189
@@ -171,9 +201,9 @@ def extract_text_from_xml(
171201 Process a Wikipedia XML dump (compressed or uncompressed) into cleaned plain text.
172202
173203 Each <page> element is parsed, its revision text is extracted,
174- cleaned using `` clean_wikitext()`` , and appended to a single
204+ cleaned using clean_wikitext(), and appended to a single
175205 output text file.
176- When * bloom_filter* and * benchmark_texts* are supplied, each
206+ When bloom_filter and benchmark_texts are supplied, each
177207 cleaned chunk is checked for contamination with evaluation
178208 benchmarks. Contaminated chunks are silently skipped.
179209
@@ -183,20 +213,9 @@ def extract_text_from_xml(
183213 Parameters
184214 ----------
185215 input_path : str or Path
186- Path to the compressed Wikipedia XML (.bz2) dump file.
187- bloom_filter : rbloom.Bloom, optional
188- Pre-built Bloom filter containing benchmark n-grams.
189- benchmark_texts : list[str], optional
190- Original benchmark text strings for exact verification.
191- n : int, optional
192- N-gram size used for contamination checking (default 13).
193- benchmarks_used : list[str], optional
194- Names of benchmark datasets used (written to manifest).
195216
196217 Output
197218 ------
198- Creates:
199- data/processed/wiki_clean.txt
200219 """
201220 input_path = Path (input_path )
202221
@@ -209,9 +228,11 @@ def extract_text_from_xml(
209228
210229 # Lazy import to avoid circular dependency at module level
211230 if bloom_filter is not None and benchmark_texts is not None :
212- from openverifiablellm .contamination import check_contamination
231+ from openverifiablellm .contamination import _normalise , check_contamination
232+ precomputed_normalised_benchmarks = {_normalise (bt ) for bt in benchmark_texts }
213233 else :
214234 check_contamination = None
235+ precomputed_normalised_benchmarks = None
215236
216237 # Fixed output path
217238 project_root = Path .cwd ()
@@ -242,8 +263,9 @@ def extract_text_from_xml(
242263 if check_contamination (
243264 cleaned ,
244265 bloom_filter ,
245- benchmark_texts ,
266+ benchmark_texts = None ,
246267 n = n ,
268+ precomputed_normalised_benchmarks = precomputed_normalised_benchmarks ,
247269 ):
248270 redacted_chunks += 1
249271 elem .clear ()
@@ -452,13 +474,89 @@ def clean_wikitext(text: str) -> str:
452474 return text .strip ()
453475
454476
455- if __name__ == "__main__" :
456- if len (sys .argv ) < 2 :
457- print ("Usage: python -m openverifiablellm.utils <input_dump> [--no-manifest]" )
477+ def run_benchmark (file_path : str , chunk_size : int = 1024 * 1024 ):
478+ logger .info ("--- Starting Benchmark ---" )
479+
480+ if not os .path .exists (file_path ):
481+ logger .error (f"Error: File not found at { file_path } " )
458482 sys .exit (1 )
459483
460- logging .basicConfig (level = logging .INFO , format = "%(levelname)s - %(message)s" )
461- extract_text_from_xml (
462- sys .argv [1 ],
463- write_manifest = "--no-manifest" not in sys .argv [2 :],
484+ size_mb = os .path .getsize (file_path ) / (1024 * 1024 )
485+
486+ logger .info (f"Benchmarking file: { file_path } " )
487+ logger .info (f"File size: { size_mb :.2f} MB" )
488+
489+ try :
490+ tracemalloc .start ()
491+
492+ # Benchmark compute_merkle_root
493+ start_time = time .perf_counter ()
494+ root_hex = compute_merkle_root (file_path , chunk_size = chunk_size )
495+ end_time = time .perf_counter ()
496+
497+ _current_mem , peak_mem = tracemalloc .get_traced_memory ()
498+
499+ root_time = end_time - start_time
500+ mins , secs = divmod (root_time , 60 )
501+ logger .info (f"compute_merkle_root ({ size_mb :.2f} MB file): { int (mins )} m { secs :.3f} s" )
502+ logger .info (f"Peak Memory Usage: { peak_mem / 10 ** 6 :.3f} MB" )
503+ logger .info (f"Merkle Root: { root_hex } " )
504+
505+ tracemalloc .reset_peak ()
506+
507+ # Benchmark generate_merkle_proof
508+ start_time = time .perf_counter ()
509+
510+ file_size_bytes = os .path .getsize (file_path )
511+ if file_size_bytes == 0 :
512+ logger .info ("Skipping proof benchmark for empty file" )
513+ else :
514+ chunk_count = (file_size_bytes + chunk_size - 1 ) // chunk_size
515+ chunk_index = min (10 , chunk_count - 1 )
516+
517+ _ = generate_merkle_proof (file_path , chunk_index = chunk_index , chunk_size = chunk_size )
518+ end_time = time .perf_counter ()
519+
520+ _ , peak_mem_proof = tracemalloc .get_traced_memory ()
521+
522+ proof_time = end_time - start_time
523+ pmins , psecs = divmod (proof_time , 60 )
524+ logger .info (
525+ f"generate_merkle_proof ({ size_mb :.2f} MB file, chunk { chunk_index } ): { int (pmins )} m { psecs :.3f} s"
526+ )
527+ logger .info (f"Peak Memory Usage for proof: { peak_mem_proof / 10 ** 6 :.3f} MB" )
528+
529+ logger .info ("--- Benchmark Complete ---" )
530+ tracemalloc .stop ()
531+
532+ except Exception :
533+ logger .exception ("An error occurred during benchmarking" )
534+ sys .exit (1 )
535+
536+
537+ if __name__ == "__main__" :
538+ parser = argparse .ArgumentParser (description = "OpenVerifiableLLM Preprocessing" )
539+ parser .add_argument ("input_dump" , help = "Path to the Wikipedia XML dump file" )
540+ parser .add_argument (
541+ "--BENCHMARK_MODE" ,
542+ type = str ,
543+ choices = ["TRUE" , "FALSE" ],
544+ default = "FALSE" ,
545+ help = "Run in benchmark mode" ,
464546 )
547+ parser .add_argument (
548+ "--chunk_size" ,
549+ type = int ,
550+ default = MERKLE_CHUNK_SIZE_BYTES ,
551+ help = "Chunk size in bytes for Merkle hashing" ,
552+ )
553+ parser .add_argument ("--no-manifest" , action = "store_true" , help = "Skip manifest generation" )
554+
555+ args = parser .parse_args ()
556+
557+ logging .basicConfig (level = logging .INFO , format = "%(levelname)s - %(message)s" )
558+
559+ if args .BENCHMARK_MODE == "TRUE" :
560+ run_benchmark (args .input_dump , args .chunk_size )
561+ else :
562+ extract_text_from_xml (args .input_dump , write_manifest = not args .no_manifest )
0 commit comments