Spaces:
Sleeping
Sleeping
| """GPU spec database loader: normalize_gpu_name() maps raw MLPerf names to canonical ids; enrich_df() joins spec columns onto a parsed MLPerf DataFrame.""" | |
| from __future__ import annotations | |
| import functools | |
| import logging | |
| import re | |
| import stat as _stat | |
| import types | |
| from pathlib import Path | |
| from typing import Optional | |
| import pandas as pd | |
| import yaml | |
| # Real spec files are ~10 KB; 1 MB cap blocks oversized files (mirrors mlperf_parser.py's _safe_read_text). | |
| _MAX_SPEC_BYTES: int = 1 * 1024 * 1024 # 1 MB | |
| log = logging.getLogger(__name__) | |
| _DEFAULT_SPEC_PATH = Path(__file__).parent.parent.parent / "data" / "gpu_specs.yaml" | |
| # Columns added by enrich_df, in output order. | |
| SPEC_COLUMNS = [ | |
| "canonical_gpu_id", | |
| "gpu_vendor", | |
| "gpu_architecture", | |
| "gpu_memory_type", | |
| "gpu_vram_gb", | |
| "gpu_hbm_bandwidth_tbps", | |
| "gpu_peak_fp32_tflops", | |
| "gpu_peak_bf16_tflops", | |
| "gpu_peak_fp16_tflops", | |
| "gpu_peak_fp8_tflops", | |
| "gpu_peak_fp6_tflops", | |
| "gpu_peak_fp4_tflops", | |
| "gpu_peak_int8_tops", | |
| "gpu_cu_sm_count", | |
| "gpu_l2_cache_mb", | |
| "gpu_tdp_w", | |
| "gpu_in_model_scope", | |
| "gpu_spec_confidence", | |
| ] | |
| # Matches "(x8)", "(x16)", "(x32)", "(x87)" etc. | |
| _COUNT_SUFFIX_RE = re.compile(r"\s*\(x\d+\)\s*$") | |
| # Matches "(Power Cap 1000 W)" and similar parenthetical power notes | |
| _POWER_SUFFIX_RE = re.compile(r"\s*\(Power Cap[^)]*\)\s*$", re.IGNORECASE) | |
| def _build_spec_cache(spec_path_str: str) -> tuple[list, dict, dict]: | |
| """Build (specs, alias_index, id_map) once per unique path string; cached objects must not be mutated by callers.""" | |
| specs = load_specs(Path(spec_path_str)) | |
| index = _build_alias_index(specs) | |
| # MappingProxyType wraps id_map values so callers can't mutate the cached spec rows. | |
| id_map = {s["id"]: types.MappingProxyType(_spec_row(s)) for s in specs} | |
| return specs, index, id_map | |
| def _load_raw(spec_path: Path | str = _DEFAULT_SPEC_PATH) -> dict: | |
| """Guarded, cached parse of the full gpu_specs.yaml dict (not just the 'gpus' list) — the one place this file is actually opened and yaml.safe_load'd. load_specs() and spec_db_version() both build on this so the same ~10 KB file isn't parsed twice for two different facts about it.""" | |
| path = Path(spec_path) | |
| try: | |
| st = path.lstat() | |
| except OSError as exc: | |
| raise FileNotFoundError(f"GPU spec DB not found: {path}") from exc | |
| if _stat.S_ISLNK(st.st_mode): | |
| raise ValueError(f"GPU spec DB path is a symlink (refused): {path}") | |
| if st.st_size > _MAX_SPEC_BYTES: | |
| raise ValueError( | |
| f"GPU spec DB too large ({st.st_size} bytes > {_MAX_SPEC_BYTES}): {path}" | |
| ) | |
| with path.open(encoding="utf-8") as f: | |
| data = yaml.safe_load(f) | |
| if not isinstance(data, dict) or "gpus" not in data: | |
| raise ValueError( | |
| f"GPU spec DB at {path} is missing required 'gpus' key " | |
| f"(got {type(data).__name__})" | |
| ) | |
| return data | |
| def load_specs(spec_path: Path | str = _DEFAULT_SPEC_PATH) -> list[dict]: | |
| """Load the GPU spec dict list from YAML; raises ValueError for symlinks, oversized files, or a missing 'gpus' key.""" | |
| return _load_raw(spec_path)["gpus"] | |
| def spec_db_version(spec_path: Path | str = _DEFAULT_SPEC_PATH) -> str: | |
| """The gpu_specs.yaml schema_version string (meta.gpu_spec_db_version).""" | |
| return str(_load_raw(spec_path).get("schema_version", "unknown")) | |
| def _build_alias_index(specs: list[dict]) -> dict[str, dict]: | |
| """Return lowercase-alias → spec dict for fast lookup.""" | |
| index: dict[str, dict] = {} | |
| for spec in specs: | |
| for alias in spec.get("aliases", []): | |
| key = alias.lower() | |
| if key in index and index[key]["id"] != spec["id"]: | |
| # Only warn when the alias maps to two *different* GPU ids (same-GPU dupes are harmless). | |
| log.warning( | |
| "Conflicting alias %r maps to both %r and %r in GPU spec DB", | |
| alias, index[key]["id"], spec["id"], | |
| ) | |
| index[key] = spec | |
| return index | |
| def _is_heterogeneous(raw_name: str) -> bool: | |
| """Return True for multi-GPU strings mixing different SKUs.""" | |
| # "A and B" pattern (heterogeneous mix) | |
| if " and " in raw_name.lower(): | |
| return True | |
| # "A (xN), B (xM)" pattern | |
| if "," in raw_name and re.search(r"\(x\d+\)", raw_name): | |
| return True | |
| return False | |
| def _strip_suffixes(raw_name: str) -> str: | |
| """Remove parenthetical count and power suffixes from a GPU name.""" | |
| name = _COUNT_SUFFIX_RE.sub("", raw_name) | |
| name = _POWER_SUFFIX_RE.sub("", name) | |
| return name.strip() | |
| def normalize_gpu_name( | |
| raw_name: str | None, | |
| specs: list[dict], | |
| *, | |
| _index: dict[str, dict] | None = None, | |
| ) -> Optional[str]: | |
| """Map a raw MLPerf accelerator_model_name to a canonical GPU id, or None for null/N/A, heterogeneous multi-GPU, or unmatched names.""" | |
| if not raw_name or raw_name.strip().upper() in {"N/A", "NA", "NONE", ""}: | |
| return None | |
| if _is_heterogeneous(raw_name): | |
| log.debug("Skipping heterogeneous GPU string: %r", raw_name) | |
| return None | |
| index = _index if _index is not None else _build_alias_index(specs) | |
| clean = _strip_suffixes(raw_name) | |
| match = index.get(clean.lower()) | |
| if match is None: | |
| log.debug("No GPU spec match for: %r (stripped: %r)", raw_name, clean) | |
| return None | |
| return match["id"] | |
| def _spec_row(spec: dict) -> dict: | |
| """Flatten one GPU spec dict into the SPEC_COLUMNS schema.""" | |
| pt = spec.get("peak_tflops") or {} | |
| return { | |
| "canonical_gpu_id": spec["id"], | |
| "gpu_vendor": spec.get("vendor"), | |
| "gpu_architecture": spec.get("architecture"), | |
| "gpu_memory_type": spec.get("memory_type"), | |
| "gpu_vram_gb": spec.get("vram_gb"), | |
| "gpu_hbm_bandwidth_tbps": spec.get("hbm_bandwidth_tbps"), | |
| "gpu_peak_fp32_tflops": pt.get("fp32"), | |
| "gpu_peak_bf16_tflops": pt.get("bf16"), | |
| "gpu_peak_fp16_tflops": pt.get("fp16"), | |
| "gpu_peak_fp8_tflops": pt.get("fp8"), | |
| "gpu_peak_fp6_tflops": pt.get("fp6"), | |
| "gpu_peak_fp4_tflops": pt.get("fp4"), | |
| "gpu_peak_int8_tops": pt.get("int8"), | |
| # Unified key for AMD compute_units / NVIDIA streaming_multiprocessors, so features stay vendor-agnostic. | |
| "gpu_cu_sm_count": ( | |
| spec.get("compute_units") or spec.get("streaming_multiprocessors") | |
| ), | |
| "gpu_l2_cache_mb": spec.get("l2_cache_mb"), | |
| "gpu_tdp_w": spec.get("tdp_w"), | |
| "gpu_in_model_scope": spec.get("in_model_scope"), | |
| "gpu_spec_confidence": spec.get("spec_confidence"), | |
| } | |
| def enrich_df( | |
| df: pd.DataFrame, | |
| spec_path: Path | str = _DEFAULT_SPEC_PATH, | |
| ) -> pd.DataFrame: | |
| """Join GPU hardware spec columns onto a parsed MLPerf DataFrame by canonical_gpu_id; unmatched rows get NaN. Returns a new DataFrame.""" | |
| specs, index, id_map = _build_spec_cache(str(Path(spec_path))) | |
| # Normalize once per unique gpu_name (not per row) — ~33 unique names across ~1223 rows. | |
| unique_names = {n for n in df["gpu_name"].dropna().unique()} | |
| name_to_id = { | |
| name: normalize_gpu_name(name, specs, _index=index) | |
| for name in unique_names | |
| } | |
| canonical_ids = df["gpu_name"].map(name_to_id) | |
| spec_df = pd.DataFrame( | |
| [id_map.get(cid, {}) for cid in canonical_ids], | |
| index=df.index, | |
| ) | |
| # Ensure all expected columns exist even if spec_rows were all empty. | |
| for col in SPEC_COLUMNS: | |
| if col not in spec_df.columns: | |
| spec_df[col] = None | |
| matched = canonical_ids.notna().sum() | |
| unmatched = canonical_ids.isna().sum() | |
| log.info( | |
| "GPU spec enrichment: %d rows matched, %d unmatched (%.0f%%)", | |
| matched, unmatched, 100 * matched / max(len(df), 1), | |
| ) | |
| if unmatched: | |
| unseen = set(df.loc[canonical_ids.isna(), "gpu_name"].dropna().unique()) | |
| # %r escapes control chars in GPU names, guarding against log injection. | |
| for name in sorted(unseen): | |
| log.debug("Unmatched gpu_name: %r", name) | |
| result = pd.concat([df, spec_df[SPEC_COLUMNS]], axis=1) | |
| # Some submitters report total system VRAM (parser's vram_gb) instead of per-GPU capacity (spec DB's gpu_vram_gb); use gpu_vram_gb for the memory-fit constraint. | |
| if "vram_gb" in result.columns and "gpu_vram_gb" in result.columns: | |
| conflict = ( | |
| result["vram_gb"].notna() | |
| & result["gpu_vram_gb"].notna() | |
| & (result["vram_gb"] != result["gpu_vram_gb"]) | |
| ) | |
| if conflict.any(): | |
| n = conflict.sum() | |
| examples = ( | |
| result.loc[conflict, ["gpu_name", "num_gpus", "vram_gb", "gpu_vram_gb"]] | |
| .drop_duplicates() | |
| ) | |
| log.warning( | |
| "%d rows: vram_gb (parser) != gpu_vram_gb (spec DB). " | |
| "Some submitters report total system VRAM. " | |
| "Use gpu_vram_gb for the memory-fit constraint.", | |
| n, | |
| ) | |
| # itertuples ~10x faster than iterrows; %r quotes gpu_name to prevent log injection. | |
| for row in examples.itertuples(index=False): | |
| log.warning( | |
| " vram conflict: gpu=%r num_gpus=%s reported=%s spec=%s", | |
| row.gpu_name, row.num_gpus, row.vram_gb, row.gpu_vram_gb, | |
| ) | |
| return result | |