Skip to content

Commit 2c2fbe6

Browse files
committed
added coderabbit changes
1 parent ebe7eb5 commit 2c2fbe6

7 files changed

Lines changed: 204 additions & 62 deletions

File tree

openverifiablellm/config.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,7 @@ class BenchmarkConfig:
2929

3030

3131
def _load_from_yaml(yaml_path: Path) -> Optional[List[str]]:
32-
"""
33-
Attempt to read benchmark identifiers from a YAML file.
34-
35-
Expected format::
36-
37-
benchmarks:
38-
- gsm8k
39-
- cais/mmlu
40-
- /path/to/local/dataset.jsonl
41-
42-
Returns:
43-
Optional[List[str]]: A list of benchmark identifiers (e.g., "gsm8k", "cais/mmlu", or local file paths)
44-
when the YAML is present and valid, or None when the file is missing, unreadable, or does not contain
45-
a "benchmarks" list.
46-
"""
32+
""" Attempt to read benchmark identifiers from a YAML file. """
4733
if not yaml_path.is_file():
4834
return None
4935

@@ -68,7 +54,6 @@ def load_config(cli_benchmarks: Optional[str] = None) -> BenchmarkConfig:
6854
"""Build a :class:`BenchmarkConfig` by resolving benchmark sources."""
6955
config = BenchmarkConfig()
7056

71-
# Priority 1: CLI flag
7257
if cli_benchmarks:
7358
benchmarks = [b.strip() for b in cli_benchmarks.split(",") if b.strip()]
7459
if benchmarks:
@@ -78,13 +63,11 @@ def load_config(cli_benchmarks: Optional[str] = None) -> BenchmarkConfig:
7863
else:
7964
logger.warning("CLI provided empty benchmarks; falling back to next priority.")
8065

81-
# Priority 2: YAML config file
8266
yaml_benchmarks = _load_from_yaml(Path(BENCHMARKS_YAML))
8367
if yaml_benchmarks:
8468
config.benchmarks = yaml_benchmarks
8569
logger.info("Benchmarks from %s: %s", BENCHMARKS_YAML, config.benchmarks)
8670
return config
8771

88-
# Priority 3: defaults
8972
logger.info("Using default benchmarks: %s", config.benchmarks)
9073
return config

openverifiablellm/contamination.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import logging
1515
import re
1616
from pathlib import Path
17-
from typing import Iterable, List, Optional, Set
17+
from typing import Dict, List, Optional, Set
1818

1919
from rbloom import Bloom
2020

@@ -58,7 +58,11 @@ class DatasetLoadError(Exception):
5858
pass
5959

6060

61-
def _load_hf(dataset_id: str, trust_remote_code: bool = False) -> List[str]:
61+
def _load_hf(
62+
dataset_id: str,
63+
trust_remote_code: bool = False,
64+
split_map: Optional[Dict[str, List[str]]] = None
65+
) -> List[str]:
6266
"""Load benchmark texts from a Hugging Face dataset."""
6367
try:
6468
from datasets import load_dataset
@@ -68,14 +72,25 @@ def _load_hf(dataset_id: str, trust_remote_code: bool = False) -> List[str]:
6872
"Install it with: pip install datasets"
6973
) from exc
7074

75+
if split_map is None:
76+
split_map = {"gsm8k": ["test"],"cais/mmlu": ["test", "validation"]}
77+
7178
texts: List[str] = []
79+
splits_to_load = split_map.get(dataset_id)
80+
7281
try:
73-
ds = load_dataset(dataset_id, trust_remote_code=trust_remote_code)
82+
if splits_to_load:
83+
splits = [
84+
load_dataset(dataset_id, split=s, trust_remote_code=trust_remote_code)
85+
for s in splits_to_load
86+
]
87+
else:
88+
ds = load_dataset(dataset_id, trust_remote_code=trust_remote_code)
89+
splits = ds.values() if hasattr(ds, "values") else [ds]
7490
except Exception as exc:
7591
logger.error("Could not load HF dataset '%s': %s", dataset_id, exc)
7692
raise DatasetLoadError(f"Failed to load HF dataset '{dataset_id}'") from exc
7793

78-
splits = ds.values() if hasattr(ds, "values") else [ds]
7994
for split in splits:
8095
for row in split:
8196
for field in _TEXT_FIELDS:
@@ -163,7 +178,9 @@ def build_bloom_filter(
163178

164179
hasher = hashlib.sha256()
165180
for text in benchmark_texts:
166-
hasher.update(text.encode("utf-8"))
181+
encoded = text.encode("utf-8")
182+
hasher.update(len(encoded).to_bytes(8, "little"))
183+
hasher.update(encoded)
167184
inputs_hash = hasher.hexdigest()
168185

169186
current_meta = {
@@ -229,8 +246,9 @@ def build_bloom_filter(
229246
def check_contamination(
230247
text: str,
231248
bloom_filter: Bloom,
232-
benchmark_texts: List[str],
249+
benchmark_texts: Optional[List[str]] = None,
233250
n: int = 13,
251+
precomputed_normalised_benchmarks: Optional[Set[str]] = None,
234252
) -> bool:
235253
"""
236254
Determine whether text is contaminated by benchmark data.
@@ -244,18 +262,19 @@ def check_contamination(
244262
performed to eliminate false positives.
245263
246264
"""
265+
if benchmark_texts is None and precomputed_normalised_benchmarks is None:
266+
raise ValueError("Must provide either benchmark_texts or precomputed_normalised_benchmarks")
267+
247268
ngrams = get_ngrams(text, n=n)
248269
if not ngrams:
249270
return False
250271

251-
normalised_benchmarks: Optional[Set[str]] = None
252-
253272
for ngram in ngrams:
254273
if ngram in bloom_filter:
255-
if normalised_benchmarks is None:
256-
normalised_benchmarks = {_normalise(bt) for bt in benchmark_texts}
274+
if precomputed_normalised_benchmarks is None:
275+
precomputed_normalised_benchmarks = {_normalise(bt) for bt in (benchmark_texts or [])}
257276

258-
for nb in normalised_benchmarks:
277+
for nb in precomputed_normalised_benchmarks:
259278
if f" {ngram} " in f" {nb} ":
260279
return True
261280

openverifiablellm/utils.py

Lines changed: 127 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import argparse
12
import bz2
23
import hashlib
34
import json
45
import logging
6+
import os
57
import platform
68
import re
79
import sys
10+
import time
11+
import tracemalloc
812
from pathlib import Path
913
from typing import Any, Dict, List, Optional, Tuple, Union
1014

@@ -24,6 +28,33 @@
2428
RE_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
2859
def 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

Comments
 (0)