Download code/scripts/p2/supply/ingest.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/viai957/align/resolve/main/code/scripts/p2/supply/ingest.py
- Command line
-
hf download hf://viai957/align/code/scripts/p2/supply/ingest.py
-
curl -L -o ingest.py https://huggingface.co/viai957/align/resolve/main/code/scripts/p2/supply/ingest.py
19.7 kB
| #!/usr/bin/env python | |
| """Stream candidate Lean statement sets into the P2 Problem JSONL schema (plan v3, Phase 0a item 4). | |
| Sources (docs/workbook/data/Datasets.json): FormalMATH-All, NuminaMath-LEAN-Sol, | |
| Nemotron-Math-Proofs-v1, RL-Goedel-Pset (levels 3-5 / 4-5), Lean4-FormalConjectures. | |
| Properties | |
| * Streaming and memory-bounded: JSONL files are read line by line over HTTP (Range-resumable, | |
| `align.data.hub.iter_lines`), parquet-only datasets through the datasets-server JSON /rows API | |
| (no pyarrow). Nothing is ever held beyond one row plus the per-source dedup set of statement shas. | |
| * Every row is pinned to the dataset revision sha (HF API, resolved once at start) and carries | |
| `provenance = {source_id, revision, row_index, license, ...}` plus an `ancestry` record | |
| (`root` = own problem_id, `parents` = []), the field `align.data.decontam` propagates to mutation | |
| children. | |
| * Output rows are PROBLEM_FIELDS (+ `provenance`, `ancestry`): `formal_statement` is | |
| `GOEDEL_HEADER + ... := by sorry` (the header is REWRITTEN when the source's differs, recorded in | |
| `provenance.header_rewritten` / `original_header`; whether the rewrite still elaborates is the | |
| elaboration check's job, `elab_hash.py` / `build_problems.py --phase check`). | |
| * Nothing is dropped silently: every drop is counted by reason in `ingest_manifest.json` and the first | |
| few examples per reason are kept in `<source>.drops.sample.json`. | |
| HF_HOME=/apps/development/rlhf/hf_cache HF_HUB_DISABLE_XET=1 \ | |
| .venv/bin/python scripts/p2/supply/ingest.py --out-dir .tmp/p2/supply --workers 2 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import importlib.util | |
| import json | |
| import logging | |
| import os | |
| import re | |
| import sys | |
| import time | |
| import urllib.request | |
| from collections import Counter | |
| from concurrent.futures import ThreadPoolExecutor | |
| from pathlib import Path | |
| from typing import Any, Callable, Iterator | |
| REPO = Path(__file__).resolve().parents[3] | |
| sys.path.insert(0, str(REPO)) | |
| os.environ.setdefault("HF_HOME", "/apps/development/rlhf/hf_cache") | |
| os.environ.setdefault("HF_HUB_DISABLE_XET", "1") | |
| def _load_common(): | |
| if "p2_common" in sys.modules: | |
| return sys.modules["p2_common"] | |
| spec = importlib.util.spec_from_file_location("p2_common", REPO / "scripts" / "p2" / "p2_common.py") | |
| mod = importlib.util.module_from_spec(spec) | |
| sys.modules["p2_common"] = mod | |
| spec.loader.exec_module(mod) | |
| return mod | |
| C = _load_common() | |
| from align.data import hub # noqa: E402 (after sys.path) | |
| log = logging.getLogger("p2.supply.ingest") | |
| MAX_STATEMENT_CHARS = 4000 # same cap as build_problems.py | |
| DEFAULT_OUT = Path(os.environ.get("ALIGN_P2_TMP", "/apps/development/rlhf/align/.tmp/p2")) / "supply" | |
| _BY_SORRY_TAIL = re.compile(r":=\s*sorry\s*$") | |
| _BY_TAIL = re.compile(r":=\s*by\s*$") | |
| # --------------------------------------------------------------------------- source registry | |
| class Source: | |
| def __init__(self, key, repo_id, license_, iterate, split="train", problem_source=None, note=""): | |
| self.key, self.repo_id, self.license, self.iterate = key, repo_id, license_, iterate | |
| self.split, self.problem_source, self.note = split, problem_source or key, note | |
| def _it_formalmath(src: Source, rev: str, args) -> Iterator[dict]: | |
| """FormalMATH_all.json is one 13 MB JSON array (the repo has no JSONL); read once, yield rows.""" | |
| req = urllib.request.Request(hub.resolve_url(src.repo_id, "FormalMATH_all.json", rev)) | |
| with urllib.request.urlopen(req, timeout=120) as r: | |
| data = json.loads(r.read()) | |
| for i, row in enumerate(data): | |
| yield {"row_index": i, "statement": row.get("autoformalization"), "header": None, | |
| "informal": row.get("refined_statement"), "name": row.get("theorem_names"), | |
| "extra": {"domain": row.get("domain"), "orig_source": row.get("source")}} | |
| def _it_numina(src: Source, rev: str, args) -> Iterator[dict]: | |
| for i, raw in enumerate(hub.iter_lines(hub.resolve_url(src.repo_id, "train.jsonl", rev))): | |
| row = json.loads(raw) if raw.strip() else None | |
| if row is None: | |
| continue | |
| yield {"row_index": i, "statement": row.get("formal_statement"), "header": None, | |
| "informal": row.get("problem"), "name": row.get("uuid"), | |
| "english": str(row.get("is_english")).lower() != "false", | |
| "extra": {"uuid": row.get("uuid"), "orig_source": row.get("source"), | |
| "statement_source": row.get("statement_source")}} | |
| def _it_nemotron(src: Source, rev: str, args) -> Iterator[dict]: | |
| """29.5 GB file (whole trajectories): never downloaded in full. `--nemotron-stripes` evenly spaced | |
| Range windows of `--nemotron-rows/stripes` lines each, so the sample is not biased to the file head. | |
| Row locator = byte offset of the line (stable for a pinned revision).""" | |
| url = hub.resolve_url(src.repo_id, "data/lean.jsonl", rev) | |
| req = urllib.request.Request(url, method="HEAD") | |
| with hub._urlopen(req) as r: # follows the redirect | |
| size = int(r.headers["Content-Length"]) | |
| stripes = max(1, args.nemotron_stripes) | |
| per = max(1, args.nemotron_rows // stripes) | |
| for s in range(stripes): | |
| start = (size // stripes) * s | |
| pos = start | |
| n = 0 | |
| first = start > 0 # the first line after a mid-file Range is a fragment: skip it | |
| for raw in hub.iter_lines(url, start_byte=start): | |
| here = pos | |
| pos += len(raw) + 1 | |
| if first: | |
| first = False | |
| continue | |
| if not raw.strip(): | |
| continue | |
| try: | |
| row = json.loads(raw) | |
| except json.JSONDecodeError: | |
| continue # truncated last line of the file only | |
| yield {"row_index": here, "statement": row.get("formal_statement"), "header": row.get("lean_header"), | |
| "informal": row.get("problem"), "name": row.get("uuid"), | |
| "extra": {"uuid": row.get("uuid"), "orig_source": row.get("source"), "byte_offset": here, | |
| "stripe": s, "row_license": row.get("license")}} | |
| n += 1 | |
| if n >= per: | |
| break | |
| def _parquet_or_server_rows(repo_id: str, rev: str, max_rows: int | None) -> Iterator[dict]: | |
| """Rows of the single `train` split. With pyarrow (run under .venv-rollout; the import lives in | |
| align.data.ingest_parquet, the one whitelisted module) the pinned parquet shard(s) are fetched once to | |
| HF_HOME and streamed in row batches. Without it: the datasets-server JSON /rows API, paced at 1 page/s | |
| with a long backoff on HTTP 429 (the server rate-limits sequential paging).""" | |
| from align.data import ingest_parquet as ip | |
| try: | |
| ip._require_pyarrow() | |
| have_pa = True | |
| except ip.IngestUnavailable: | |
| have_pa = False | |
| if have_pa: | |
| dest = Path(os.environ["HF_HOME"]) / "align_streams" / "supply" / repo_id.replace("/", "__") / rev | |
| dest.mkdir(parents=True, exist_ok=True) | |
| files = [e for e in hub.list_repo_files(repo_id, rev, "data") if e.get("type") == "file" and e["path"].endswith(".parquet")] | |
| emitted = 0 | |
| for e in sorted(files, key=lambda e: e["path"]): | |
| target = dest / Path(e["path"]).name | |
| want = (e.get("lfs") or {}).get("oid") | |
| if not (target.exists() and target.stat().st_size == e["size"]): | |
| tmp = target.with_name(target.name + f".tmp{os.getpid()}") | |
| try: | |
| with urllib.request.urlopen(hub.resolve_url(repo_id, e["path"], rev), timeout=300) as r, open(tmp, "wb") as f: | |
| while chunk := r.read(4 << 20): | |
| f.write(chunk) | |
| if want and C.sha256_file(tmp) != want: | |
| raise RuntimeError(f"{e['path']}: sha256 differs from the hub's LFS oid") | |
| os.replace(tmp, target) | |
| finally: | |
| if tmp.exists(): | |
| tmp.unlink() | |
| for row in ip.iter_parquet_rows(target, prefer_ingested=False): | |
| if max_rows and emitted >= max_rows: | |
| return | |
| emitted += 1 | |
| yield row | |
| return | |
| offset, emitted = 0, 0 | |
| while not max_rows or emitted < max_rows: | |
| for attempt in range(8): | |
| try: | |
| page = hub.fetch_rows_page(repo_id, "default", "train", offset, 100, use_cache=False) | |
| break | |
| except hub.HubError as err: | |
| if "429" not in str(err) or attempt == 7: | |
| raise | |
| time.sleep(60) | |
| rows = page["rows"] | |
| if not rows: | |
| return | |
| for r in rows: | |
| if r.get("truncated_cells"): | |
| raise hub.HubError(f"truncated cells at row {r.get('row_idx')}") | |
| if max_rows and emitted >= max_rows: | |
| return | |
| emitted += 1 | |
| yield r["row"] | |
| offset += len(rows) | |
| if offset >= page.get("num_rows_total", offset + 1): | |
| return | |
| time.sleep(1.0) | |
| def _it_rows(src: Source, rev: str, args) -> Iterator[dict]: | |
| for i, row in enumerate(_parquet_or_server_rows(src.repo_id, rev, args.max_rows)): | |
| yield {"row_index": i, "statement": row.get("formal_statement"), "header": None, | |
| "informal": row.get("informal_statement"), "name": row.get("problem_id"), | |
| "extra": {"orig_problem_id": row.get("problem_id"), "orig_source": row.get("source")}} | |
| def _it_conjectures(src: Source, rev: str, args) -> Iterator[dict]: | |
| """Rows are declarations: statement = everything after the name (binders + type).""" | |
| for i, row in enumerate(_parquet_or_server_rows(src.repo_id, rev, args.max_rows)): | |
| kind = (row.get("type") or "").strip() | |
| imports = row.get("imports") or "[]" | |
| yield {"row_index": i, "statement": None, "header": None, "informal": row.get("docstring") or None, | |
| "name": row.get("symbolic_name"), "decl_kind": kind, "decl_tail": row.get("statement"), | |
| "imports": imports, | |
| "extra": {"library": row.get("library"), "filename": row.get("filename"), | |
| "source_url": row.get("source_url"), "upstream_commit": row.get("commit")}} | |
| SOURCES: dict[str, Source] = {s.key: s for s in [ | |
| Source("formalmath-all", "SphereLab/FormalMATH-All", "mit", _it_formalmath), | |
| Source("numinamath-lean-sol", "iiis-lean/NuminaMath-LEAN-Sol", "apache-2.0", _it_numina), | |
| Source("nemotron-math-proofs-v1", "nvidia/Nemotron-Math-Proofs-v1", "cc-by-sa-4.0", _it_nemotron, | |
| note="sampled in Range stripes; the full file is 29.5 GB of trajectories"), | |
| Source("rl-goedel-pset-l45", "HangHor/RL-Goedel-Pset-Level-4-5", "mit", _it_rows, | |
| note="levels 4-5 proper (46,290 rows)"), | |
| Source("rl-goedel-pset-l35", "HangHor/RL-Goedel-Pset-Level-3-5", "mit", _it_rows, | |
| note="levels 3-5 superset of l45; not in the default set"), | |
| Source("lean4-formalconjectures", "phanerozoic/Lean4-FormalConjectures", "apache-2.0", _it_conjectures), | |
| ]} | |
| DEFAULT_SOURCES = ["formalmath-all", "numinamath-lean-sol", "nemotron-math-proofs-v1", "rl-goedel-pset-l45", | |
| "lean4-formalconjectures"] | |
| def repo_revision(repo_id: str) -> str: | |
| with urllib.request.urlopen(f"https://huggingface.co/api/datasets/{repo_id}", timeout=60) as r: | |
| return json.loads(r.read())["sha"] | |
| # --------------------------------------------------------------------------- pure conversion | |
| def assemble_statement(rec: dict) -> tuple[str, dict]: | |
| """-> (candidate full source text, extra provenance). Raises ContractError('drop:<reason>').""" | |
| extra: dict[str, Any] = {} | |
| if rec.get("decl_kind") is not None: # FormalConjectures declaration | |
| kind = rec["decl_kind"] | |
| if kind not in ("theorem", "lemma"): | |
| raise C.ContractError("drop:not_theorem_or_lemma") | |
| imps = str(rec.get("imports", "")).lower() | |
| if "formalconjectures" in imps or "formal_conjectures" in imps: | |
| raise C.ContractError("drop:needs_nonmathlib_imports") | |
| if not rec.get("name") or not rec.get("decl_tail"): | |
| raise C.ContractError("drop:empty_statement") | |
| raw = f"{kind} {rec['name']} {rec['decl_tail'].strip()} := by sorry" | |
| return C.GOEDEL_HEADER + raw, {"constructed_from_declaration": True} | |
| raw = rec.get("statement") | |
| if not isinstance(raw, str) or not raw.strip(): | |
| raise C.ContractError("drop:empty_statement") | |
| raw = raw.strip("\n") | |
| if rec.get("header"): | |
| raw = rec["header"].rstrip() + "\n\n" + raw.lstrip() | |
| if _BY_SORRY_TAIL.search(raw): # `:= sorry` term-mode placeholder | |
| raw = _BY_SORRY_TAIL.sub(":= by sorry", raw) | |
| extra["sorry_term_rewritten"] = True | |
| elif _BY_TAIL.search(raw): # statement stored without the placeholder | |
| raw = raw.rstrip() + " sorry" | |
| extra["sorry_appended"] = True | |
| elif ":= by" not in raw and re.search(r"\bsorry\b", raw) is None: | |
| raw = raw.rstrip() + " := by sorry" # statement stored without any body | |
| extra["body_appended"] = True | |
| return raw, extra | |
| def _open_names(header: str) -> set[str]: | |
| out: set[str] = set() | |
| for ln in header.splitlines(): | |
| ln = ln.strip() | |
| if ln.startswith("open "): | |
| out.update(ln[5:].split()) | |
| return out | |
| def to_problem(rec: dict, source: Source, revision: str, pid: str) -> tuple[dict, dict]: | |
| """Full conversion of one raw row -> (Problem record, side info). Raises ContractError('drop:<reason>').""" | |
| raw, extra = assemble_statement(rec) | |
| if len(raw) > MAX_STATEMENT_CHARS: | |
| raise C.ContractError("drop:too_long") | |
| try: | |
| stmt = C.normalize_formal_statement(raw) | |
| except C.ContractError: | |
| raise C.ContractError("drop:not_single_by_sorry") | |
| try: | |
| name, header, _sig = C.statement_key(stmt) | |
| except C.ContractError as e: | |
| why = re.sub(r"`[^`]*`", "`X`", str(e).replace("unsupported statement: ", "")) | |
| raise C.ContractError(f"drop:unsupported_shape: {why}"[:120]) | |
| prov_header = {} | |
| if header.rstrip() != C.GOEDEL_HEADER.rstrip(): | |
| parsed = C._strict().parse_statement(stmt) | |
| body = stmt[len(parsed.header_text):].lstrip("\n") | |
| stmt = C.GOEDEL_HEADER + body | |
| prov_header = {"header_rewritten": True, "original_header": header.strip()[:600]} | |
| extra_open = sorted(_open_names(header) - _open_names(C.GOEDEL_HEADER)) | |
| if extra_open: # the rewrite drops namespaces the statement may rely on: elaboration decides | |
| prov_header["header_extra_opens"] = extra_open | |
| try: # the rewritten statement must still be a supported shape | |
| C.statement_key(stmt) | |
| except C.ContractError: | |
| raise C.ContractError("drop:unsupported_after_header_rewrite") | |
| prob = { | |
| "problem_id": pid, "source": source.problem_source, "split": "train", "formal_statement": stmt, | |
| "informal_prefix": rec.get("informal") if isinstance(rec.get("informal"), str) and rec["informal"].strip() else None, | |
| "statement_sha": C.statement_sha(stmt), | |
| "provenance": {"source_id": source.repo_id, "revision": revision, "row_index": rec["row_index"], | |
| "license": source.license, "theorem_name": name, **extra, **prov_header, **rec.get("extra", {})}, | |
| "ancestry": {"root": pid, "parents": []}, | |
| } | |
| C.validate_problem(prob) | |
| return prob, {"header_rewritten": bool(prov_header)} | |
| def ingest_source(src: Source, revision: str, out_dir: Path, args) -> dict: | |
| counts: Counter = Counter() | |
| drop_examples: dict[str, list] = {} | |
| seen: set[str] = set() | |
| t0 = time.time() | |
| out_path = out_dir / f"{src.key}.jsonl" | |
| tmp = out_path.with_name(out_path.name + f".tmp{os.getpid()}") | |
| try: | |
| with open(tmp, "w", encoding="utf-8") as f: | |
| for rec in src.iterate(src, revision, args): | |
| counts["rows_seen"] += 1 | |
| if rec.get("english") is False: | |
| counts["drop:not_english"] += 1 | |
| continue | |
| pid = f"{src.key}-{rec['row_index']}" | |
| try: | |
| prob, side = to_problem(rec, src, revision, pid) | |
| except C.ContractError as e: | |
| why = str(e) | |
| if not why.startswith("drop:"): | |
| raise | |
| counts[why] += 1 | |
| ex = drop_examples.setdefault(why, []) | |
| if len(ex) < 3: | |
| ex.append({"row_index": rec["row_index"], "name": rec.get("name")}) | |
| continue | |
| if prob["statement_sha"] in seen: | |
| counts["drop:duplicate_statement"] += 1 | |
| continue | |
| seen.add(prob["statement_sha"]) | |
| f.write(json.dumps(prob, ensure_ascii=False) + "\n") | |
| counts["kept"] += 1 | |
| counts["header_rewritten"] += int(side["header_rewritten"]) | |
| counts["header_extra_opens"] += int("header_extra_opens" in prob["provenance"]) | |
| if counts["rows_seen"] % 20000 == 0: | |
| log.info("%s: %d rows, %d kept", src.key, counts["rows_seen"], counts["kept"]) | |
| f.flush() | |
| os.fsync(f.fileno()) | |
| os.replace(tmp, out_path) | |
| finally: | |
| if tmp.exists(): | |
| tmp.unlink() | |
| (out_dir / f"{src.key}.drops.sample.json").write_text(json.dumps(drop_examples, indent=1, ensure_ascii=False)) | |
| return {"source_id": src.repo_id, "revision": revision, "license": src.license, "note": src.note, | |
| "counts": dict(counts), "seconds": round(time.time() - t0, 1), "output": str(out_path), | |
| "output_sha256": C.sha256_file(out_path)} | |
| def main(argv=None) -> int: | |
| ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) | |
| ap.add_argument("--out-dir", default=str(DEFAULT_OUT)) | |
| ap.add_argument("--sources", nargs="+", default=DEFAULT_SOURCES, choices=sorted(SOURCES)) | |
| ap.add_argument("--workers", type=int, default=2, help="parallel sources; hard-capped at 2") | |
| ap.add_argument("--max-rows", type=int, default=0, help="cap rows per datasets-server source (0 = all)") | |
| ap.add_argument("--nemotron-rows", type=int, default=60000) | |
| ap.add_argument("--nemotron-stripes", type=int, default=8) | |
| a = ap.parse_args(argv) | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s") | |
| out_dir = Path(a.out_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| revs = {k: repo_revision(SOURCES[k].repo_id) for k in a.sources} | |
| log.info("revisions: %s", revs) | |
| workers = max(1, min(2, a.workers)) | |
| results: dict[str, Any] = {} | |
| with ThreadPoolExecutor(max_workers=workers) as ex: | |
| futs = {k: ex.submit(ingest_source, SOURCES[k], revs[k], out_dir, a) for k in a.sources} | |
| for k, fu in futs.items(): | |
| try: | |
| results[k] = fu.result() | |
| log.info("%s: %s", k, results[k]["counts"]) | |
| except Exception as e: # one failed source must not discard the others' results | |
| log.error("%s FAILED: %r", k, e) | |
| results[k] = {"source_id": SOURCES[k].repo_id, "revision": revs[k], "error": repr(e)} | |
| man = out_dir / "ingest_manifest.json" | |
| prev = json.loads(man.read_text()) if man.exists() else {} | |
| prev.update(results) | |
| man.write_text(json.dumps(prev, indent=1, ensure_ascii=False)) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |