Nadav-marketaem's picture
Add training and inference code
07e8637
Raw
History Blame Contribute Delete
17.3 kB
"""
Stage 5 β€” Filter FineWeb by classifier score and write release datasets.
Reads per-dump score parquets produced by Stage 4 and streams through FineWeb
to produce two filtered datasets:
fineweb-marketing top ~8% by Head A percentile (~1.3T tokens)
fineweb-marketing-score-2 top ~35% by Head A percentile (~5.4T tokens)
Filtering is percentile-based, not absolute int_score (spec Β§5.2). The three
score columns (score, int_score, percentile) are kept in every output row
alongside all original FineWeb document columns.
Crash-safe: both output files must exist for a dump to be considered complete;
if either is missing the dump is reprocessed from scratch.
Usage β€” single dump:
python -m filtering.filter \\
--scores-dir data/scores \\
--output-dir data/release \\
--dump CC-MAIN-2024-10
Usage β€” all dumps (discovers via HF Hub):
python -m filtering.filter \\
--scores-dir data/scores \\
--output-dir data/release \\
--all-dumps
Usage β€” stats only (threshold statistics without streaming FineWeb):
python -m filtering.filter \\
--scores-dir data/scores \\
--output-dir data/release \\
--all-dumps --stats-only
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
import numpy as np
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
from datasets import get_dataset_config_names, load_dataset
from tqdm import tqdm
FINEWEB_REPO = "HuggingFaceFW/fineweb"
PRIMARY_PCT = 0.08 # top 8% β†’ FineWeb-Marketing
SECONDARY_PCT = 0.35 # top 35% β†’ FineWeb-Marketing-score-2
PRIMARY_SUBDIR = "fineweb-marketing"
SECONDARY_SUBDIR = "fineweb-marketing-score-2"
WRITE_CHUNK = 10_000
# ── Pure functions (testable without network) ──────────────────────────────────
def compute_percentile_threshold(target_fraction: float) -> float:
"""
Convert a retention fraction to the minimum percentile for kept documents.
Top 8% β†’ 0.92. Raises ValueError for out-of-range input.
"""
if not 0.0 < target_fraction < 1.0:
raise ValueError(
f"target_fraction must be in (0, 1), got {target_fraction!r}"
)
return 1.0 - target_fraction
def build_score_index(
scores_path: Path,
min_percentile: float,
) -> dict[str, tuple[float, int, float]]:
"""
Load a Stage 4 score parquet and return {id: (score, int_score, percentile)}
for all rows with percentile >= min_percentile.
Uses PyArrow predicate pushdown to avoid loading below-threshold rows into RAM.
The threshold is converted to float32 space before filtering to stay consistent
with the stored float32 column and avoid boundary mismatches.
"""
# Convert to float32 space so the filter matches the stored column type
threshold_f32 = float(np.float32(min_percentile))
table = pq.read_table(
scores_path,
columns=["id", "score", "int_score", "percentile"],
filters=[("percentile", ">=", threshold_f32)],
)
ids = table.column("id").to_pylist()
scores = table.column("score").to_pylist()
int_scores = table.column("int_score").to_pylist()
percentiles = table.column("percentile").to_pylist()
return {
id_: (float(score), int(int_score), float(pct))
for id_, score, int_score, pct in zip(ids, scores, int_scores, percentiles)
}
def route_doc(
doc_id: str,
score_index: dict[str, tuple[float, int, float]],
primary_threshold: float,
) -> tuple[str, tuple[float, int, float]] | None:
"""
Classify a document as "primary" or "secondary" and return its score entry.
"primary" β†’ in index with percentile >= primary_threshold
"secondary" β†’ in index with percentile < primary_threshold
None β†’ not in index (below secondary threshold)
The score_index is already filtered to >= secondary_threshold, so any ID
absent from it is automatically excluded. Returning the entry alongside
the tier avoids a second dict lookup at the call site.
"""
entry = score_index.get(doc_id)
if entry is None:
return None
_, _, pct = entry
tier = "primary" if pct >= primary_threshold else "secondary"
return tier, entry
def compute_threshold_stats(
scores_path: Path,
primary_threshold: float,
secondary_threshold: float,
) -> dict[str, Any]:
"""
Compute per-dump doc counts and minimum score at each percentile cutoff.
The returned int_score distributions reveal the equivalent int_score
thresholds for FineWeb-EDU users (written to threshold_summary.json).
"""
table = pq.read_table(scores_path, columns=["score", "int_score", "percentile"])
df = table.to_pandas()
p_mask = df["percentile"] >= primary_threshold
s_mask = df["percentile"] >= secondary_threshold
def _int_counts(mask: pd.Series) -> dict[str, int]:
if not mask.any():
return {}
return {
str(int(k)): int(v)
for k, v in df.loc[mask, "int_score"].value_counts().sort_index().items()
}
return {
"total_docs": int(len(df)),
"primary_docs": int(p_mask.sum()),
"secondary_docs": int(s_mask.sum()),
"primary_min_score": (
float(df.loc[p_mask, "score"].min()) if p_mask.any() else None
),
"secondary_min_score": (
float(df.loc[s_mask, "score"].min()) if s_mask.any() else None
),
"primary_int_score_counts": _int_counts(p_mask),
"secondary_int_score_counts": _int_counts(s_mask),
}
# ── Buffered parquet writer ────────────────────────────────────────────────────
class _TierWriter:
"""Accumulates rows in memory and flushes to a single parquet file in chunks."""
def __init__(self, path: Path, chunk_size: int = WRITE_CHUNK) -> None:
self._path = path
self._chunk_size = chunk_size
self._buf: list[dict[str, Any]] = []
self._writer: pq.ParquetWriter | None = None
self._schema: pa.Schema | None = None
self.count: int = 0
def add(self, row: dict[str, Any]) -> None:
self._buf.append(row)
self.count += 1
if len(self._buf) >= self._chunk_size:
self._flush()
def _flush(self) -> None:
if not self._buf:
return
columns = list(self._buf[0].keys())
table = pa.Table.from_arrays(
[pa.array([r[c] for r in self._buf]) for c in columns],
names=columns,
)
if self._writer is None:
self._schema = table.schema
self._writer = pq.ParquetWriter(
self._path, self._schema, compression="snappy"
)
else:
table = table.cast(self._schema)
self._writer.write_table(table)
self._buf.clear()
def close(self) -> None:
"""Flush remaining rows and close. Writes a zero-row file if no data was added."""
self._flush()
if self._writer is not None:
self._writer.close()
else:
_write_empty(self._path)
def _write_empty(path: Path) -> None:
"""Write a zero-row parquet to mark a dump as complete when no docs pass the threshold."""
schema = pa.schema([
("id", pa.string()),
("dump", pa.string()),
("score", pa.float32()),
("int_score", pa.int8()),
("percentile", pa.float32()),
])
pq.write_table(
pa.table(
{f: pa.array([], type=t) for f, t in zip(schema.names, schema.types)},
schema=schema,
),
path,
)
# ── Main filter function ───────────────────────────────────────────────────────
def filter_dump(
dump: str,
scores_dir: Path,
output_dir: Path,
primary_pct: float = PRIMARY_PCT,
secondary_pct: float = SECONDARY_PCT,
hf_dataset: str = FINEWEB_REPO,
) -> dict[str, Any]:
"""
Filter one CC-MAIN dump into the primary and secondary release datasets.
Phase 1: load the Stage 4 score parquet, build an in-memory index for all
documents above the secondary percentile threshold.
Phase 2: stream through FineWeb once, routing each matching document to the
primary and/or secondary output writer.
Both output files must exist for the dump to be considered complete.
Temp files are used for atomic rename; stale temps from a prior crash are
removed at the start of each run.
"""
primary_out = output_dir / PRIMARY_SUBDIR
secondary_out = output_dir / SECONDARY_SUBDIR
primary_file = primary_out / f"{dump}.parquet"
secondary_file = secondary_out / f"{dump}.parquet"
if primary_file.exists() and secondary_file.exists():
print(f" {dump}: already done, skipping.")
return {"dump": dump, "status": "skipped"}
scores_path = scores_dir / f"{dump}.parquet"
if not scores_path.exists():
print(f" {dump}: no score file found at {scores_path}, skipping.")
return {"dump": dump, "status": "no_scores"}
primary_out.mkdir(parents=True, exist_ok=True)
secondary_out.mkdir(parents=True, exist_ok=True)
primary_threshold = compute_percentile_threshold(primary_pct)
secondary_threshold = compute_percentile_threshold(secondary_pct)
score_index = build_score_index(scores_path, secondary_threshold)
print(
f" {dump}: {len(score_index):,} docs pass secondary threshold "
f"({secondary_pct:.0%})"
)
primary_tmp = primary_file.with_suffix(".parquet.tmp")
secondary_tmp = secondary_file.with_suffix(".parquet.tmp")
for tmp in (primary_tmp, secondary_tmp):
tmp.unlink(missing_ok=True)
primary_writer = _TierWriter(primary_tmp, chunk_size=WRITE_CHUNK)
secondary_writer = _TierWriter(secondary_tmp, chunk_size=WRITE_CHUNK)
streamed = 0
ds = load_dataset(hf_dataset, name=dump, split="train", streaming=True)
try:
for doc in tqdm(ds, desc=f"Filtering {dump}"):
streamed += 1
routed = route_doc(doc["id"], score_index, primary_threshold)
if routed is None:
continue
tier, (score, int_score, percentile) = routed
row = dict(doc)
row["score"] = np.float32(score)
row["int_score"] = np.int8(int_score)
row["percentile"] = np.float32(percentile)
if tier == "primary":
primary_writer.add(row)
secondary_writer.add(row)
finally:
primary_writer.close()
secondary_writer.close()
# Only reached if streaming completed without exception
primary_tmp.rename(primary_file)
secondary_tmp.rename(secondary_file)
print(
f" {dump}: {primary_writer.count:,} primary, "
f"{secondary_writer.count:,} secondary "
f"(of {streamed:,} streamed)"
)
return {
"dump": dump,
"status": "done",
"primary": primary_writer.count,
"secondary": secondary_writer.count,
"streamed": streamed,
}
# ── Threshold summary ──────────────────────────────────────────────────────────
def write_threshold_summary(
per_dump_stats: list[dict[str, Any]],
primary_pct: float,
secondary_pct: float,
output_path: Path,
) -> None:
"""
Write a JSON file documenting per-dump score and int_score statistics at
both percentile cutoffs.
This documents the equivalent int_score thresholds so FineWeb-EDU users
can compare datasets without re-running inference.
"""
output_path.write_text(
json.dumps(
{
"primary_fraction": primary_pct,
"secondary_fraction": secondary_pct,
"primary_percentile_threshold": compute_percentile_threshold(primary_pct),
"secondary_percentile_threshold": compute_percentile_threshold(
secondary_pct
),
"per_dump": per_dump_stats,
},
indent=2,
)
)
print(f"Threshold summary β†’ {output_path}")
# ── Dump discovery ─────────────────────────────────────────────────────────────
def get_cc_dumps(hf_dataset: str = FINEWEB_REPO) -> list[str]:
"""Return sorted list of CC-MAIN-* config names for the given HF dataset."""
configs = get_dataset_config_names(hf_dataset)
return sorted(c for c in configs if c.startswith("CC-MAIN-"))
# ── CLI ────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
ap = argparse.ArgumentParser(
description="Stage 5: filter FineWeb by Head A score and write release datasets."
)
ap.add_argument(
"--scores-dir",
type=Path,
required=True,
help="Directory with per-dump score parquets from Stage 4",
)
ap.add_argument(
"--output-dir",
type=Path,
required=True,
help=(
"Root output directory. Two subdirectories are created: "
f"{PRIMARY_SUBDIR!r} and {SECONDARY_SUBDIR!r}."
),
)
dump_group = ap.add_mutually_exclusive_group(required=True)
dump_group.add_argument("--dump", help="Single CC-MAIN dump name")
dump_group.add_argument(
"--all-dumps",
action="store_true",
help="Discover and process all CC-MAIN configs from HF Hub",
)
dump_group.add_argument(
"--dump-list",
type=Path,
help="Plain-text file with one dump name per line",
)
ap.add_argument(
"--primary-pct",
type=float,
default=PRIMARY_PCT,
help=f"Primary retention fraction (default {PRIMARY_PCT})",
)
ap.add_argument(
"--secondary-pct",
type=float,
default=SECONDARY_PCT,
help=f"Secondary retention fraction (default {SECONDARY_PCT})",
)
ap.add_argument("--hf-dataset", default=FINEWEB_REPO)
ap.add_argument(
"--stats-only",
action="store_true",
help="Print threshold statistics from score parquets only (no FineWeb streaming)",
)
args = ap.parse_args()
if args.dump:
dumps = [args.dump]
elif args.dump_list:
dumps = [
d.strip()
for d in args.dump_list.read_text().splitlines()
if d.strip()
]
else:
print(f"Discovering CC-MAIN dumps from {args.hf_dataset} …")
dumps = get_cc_dumps(args.hf_dataset)
print(f"Found {len(dumps)} CC-MAIN dumps.")
primary_threshold = compute_percentile_threshold(args.primary_pct)
secondary_threshold = compute_percentile_threshold(args.secondary_pct)
args.output_dir.mkdir(parents=True, exist_ok=True)
summary_path = args.output_dir / "threshold_summary.json"
if args.stats_only:
per_dump_stats = []
for d in dumps:
p = args.scores_dir / f"{d}.parquet"
if not p.exists():
print(f" {d}: no score file, skipping.")
continue
stats = compute_threshold_stats(p, primary_threshold, secondary_threshold)
stats["dump"] = d
per_dump_stats.append(stats)
print(
f" {d}: {stats['primary_docs']:,} primary / "
f"{stats['secondary_docs']:,} secondary of "
f"{stats['total_docs']:,} total"
)
write_threshold_summary(
per_dump_stats, args.primary_pct, args.secondary_pct, summary_path
)
else:
all_stats = []
for d in dumps:
result = filter_dump(
dump=d,
scores_dir=args.scores_dir,
output_dir=args.output_dir,
primary_pct=args.primary_pct,
secondary_pct=args.secondary_pct,
hf_dataset=args.hf_dataset,
)
# Include stats for both newly processed and already-skipped dumps
# so threshold_summary.json is complete even on resumed runs.
if result.get("status") in ("done", "skipped"):
score_p = args.scores_dir / f"{d}.parquet"
if score_p.exists():
dump_stats = compute_threshold_stats(
score_p, primary_threshold, secondary_threshold
)
dump_stats["dump"] = d
all_stats.append(dump_stats)
if all_stats:
write_threshold_summary(
all_stats, args.primary_pct, args.secondary_pct, summary_path
)
print("Stage 5 complete.")