gpu-perf-prophet / src /data /manifest.py
vxa8502's picture
Code cleanup
0623ff0
Raw
History Blame Contribute Delete
8.37 kB
"""Data manifest — pins source snapshots (git commit or SHA-256) for reproducibility; verification is warn-only (not hard-refuse) since a single-developer project can't enforce refresh-then-relock discipline, but symlinked inputs are always refused outright, same as gpu_spec_db/recommender/predictor's integrity guards."""
from __future__ import annotations
import hashlib
import logging
import stat as _stat
import subprocess
from pathlib import Path
from typing import Optional
import yaml
log = logging.getLogger(__name__)
MLPERF_ROUNDS = ["v4.1", "v5.0", "v5.1", "v6.0"]
MLPERF_ROOT = Path("data/raw/mlperf")
CALIBRATION_CSV = Path("benchmarks/mi300x_calibration_results.csv")
MANIFEST_PATH = Path("data/data_manifest.lock")
# Calibration CSVs/lock files are KB-scale; 1 MB matches gpu_spec_db.py/recommender.py's cap for gpu_specs.yaml/pricing.yaml.
_MAX_HASHED_FILE_BYTES = 1 * 1024 * 1024 # 1 MB
# The training corpus grows with every merged round/batch, so it gets its own larger cap; 20 MB is generous headroom over the current ~115 KB file.
_MAX_CORPUS_BYTES = 20 * 1024 * 1024 # 20 MB
def _refuse_symlink_and_oversize(path: Path, max_bytes: int) -> None:
"""Mirror gpu_spec_db.load_specs / recommender._load_pricing's guard: a single lstat() refuses symlinks and oversized files before this module hashes/parses a trust-anchor file."""
try:
st = path.lstat()
except OSError as exc:
raise FileNotFoundError(f"Path not found: {path}") from exc
if _stat.S_ISLNK(st.st_mode):
raise ValueError(f"Refusing to read a symlinked path: {path}")
if st.st_size > max_bytes:
raise ValueError(f"{path} too large ({st.st_size} bytes > {max_bytes})")
def _run_git(repo_dir: Path, *args: str) -> Optional[str]:
try:
out = subprocess.run(
["git", "-C", str(repo_dir), *args],
capture_output=True, text=True, timeout=10, check=True,
)
return out.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
return None
def _file_sha256(path: Path, max_bytes: Optional[int] = None) -> Optional[str]:
if not path.is_file():
return None
# Resolved inside the call (not a bound default) so tests that monkeypatch _MAX_HASHED_FILE_BYTES still take effect.
if max_bytes is None:
max_bytes = _MAX_HASHED_FILE_BYTES
_refuse_symlink_and_oversize(path, max_bytes)
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def corpus_sha256(path: Path) -> Optional[str]:
"""SHA-256 of a built training corpus file for the model sidecar's ``corpus_sha256`` field, using the larger ``_MAX_CORPUS_BYTES`` cap; returns None if the corpus isn't present (normal on a fresh clone)."""
return _file_sha256(path, max_bytes=_MAX_CORPUS_BYTES)
def _mlperf_source(round_tag: str) -> dict:
repo_dir = MLPERF_ROOT / round_tag
# Same as mlperf_parser.py's walk guard — a symlinked round dir could let `git -C` report an arbitrary repo as the pinned mirror.
if repo_dir.is_symlink():
raise ValueError(f"Refusing to read a symlinked MLPerf mirror directory: {repo_dir}")
# One `git log` call gets commit hash + date via \x1f delimiter, saving a subprocess spawn (measured 12 spawns/260ms across 4 rounds before this).
log_out = _run_git(repo_dir, "log", "-1", "--format=%H\x1f%cI")
if log_out and "\x1f" in log_out:
commit, commit_date = log_out.split("\x1f", 1)
else:
commit, commit_date = None, None
url = _run_git(repo_dir, "remote", "get-url", "origin")
return {
"type": "git",
"path": str(repo_dir),
"url": url,
"commit": commit,
"snapshot_date": commit_date[:10] if commit_date else None,
"present": repo_dir.is_dir(),
}
def _calibration_source() -> dict:
return {
"type": "file",
"path": str(CALIBRATION_CSV),
"url": "self_run:amd_dev_cloud",
"sha256": _file_sha256(CALIBRATION_CSV),
"snapshot_date": None,
"present": CALIBRATION_CSV.is_file(),
}
def compute_manifest() -> dict:
"""Build a manifest reflecting the current state of every known source."""
sources = {f"mlperf_{tag.replace('.', '_')}": _mlperf_source(tag) for tag in MLPERF_ROUNDS}
sources["mi300x_calibration"] = _calibration_source()
return {"sources": sources}
def write_manifest(path: Path = MANIFEST_PATH) -> dict:
"""Compute the current manifest and persist it as the locked snapshot."""
manifest = compute_manifest()
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w") as f:
yaml.safe_dump(manifest, f, sort_keys=False, default_flow_style=False)
log.info("Wrote data manifest → %s", path)
return manifest
def load_manifest(path: Path = MANIFEST_PATH) -> Optional[dict]:
if not path.is_file():
return None
# The lock file is the trust anchor verify_manifest() checks everything else against, so it gets the same symlink/size guard as _file_sha256.
_refuse_symlink_and_oversize(path, _MAX_HASHED_FILE_BYTES)
with path.open() as f:
return yaml.safe_load(f)
def _identity(entry: dict) -> Optional[str]:
"""The value that must stay stable for a source to be considered unchanged."""
return entry.get("commit") if entry.get("type") == "git" else entry.get("sha256")
def sources_present(
path: Path = MANIFEST_PATH, locked: Optional[dict] = None
) -> tuple[int, int]:
"""(present, total) count of locked sources that exist on disk right now — gives context to train_final.py's flattened manifest_verified boolean; cheap presence-only check (no git spawns/hashing); pass an already-loaded ``locked`` dict to avoid re-reading the lock file."""
if locked is None:
locked = load_manifest(path)
if locked is None:
return 0, 0
sources = locked.get("sources", {})
present = 0
for entry in sources.values():
p = Path(entry["path"])
if p.is_symlink():
continue # a tampered/symlinked path is not a genuine "present" source
is_present = p.is_dir() if entry.get("type") == "git" else p.is_file()
if is_present:
present += 1
return present, len(sources)
def verify_manifest(path: Path = MANIFEST_PATH, locked: Optional[dict] = None) -> list[str]:
"""Compare the locked manifest against current source state; returns human-readable mismatch descriptions (absent sources aren't drift) and logs a WARNING per mismatch, but never raises (warn-only by design — see module docstring)."""
if locked is None:
locked = load_manifest(path)
if locked is None:
log.warning("No data manifest found at %s — skipping source-drift check.", path)
return []
current = compute_manifest()
mismatches: list[str] = []
for name, locked_entry in locked.get("sources", {}).items():
current_entry = current["sources"].get(name)
if current_entry is None:
continue
if not current_entry.get("present"):
continue # not fetched locally — not a drift, just absent
locked_id = _identity(locked_entry)
current_id = _identity(current_entry)
if locked_id is None or current_id is None:
continue
if locked_id != current_id:
msg = (
f"data source '{name}' has drifted from data_manifest.lock: "
f"locked={locked_id[:12]} current={current_id[:12]}"
)
mismatches.append(msg)
log.warning(msg)
if mismatches:
log.warning(
"%d data source(s) drifted from the locked manifest (%s). "
"Training will proceed (warn-only by design) — "
"if this drift wasn't intentional, results may not reproduce prior runs. "
"Regenerate the lock file with `python -m src.data.manifest` once you've "
"confirmed the new source state is intentional.",
len(mismatches), path,
)
return mismatches
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
write_manifest()
if __name__ == "__main__":
main()