Spaces:
Sleeping
Sleeping
| """Reliability-target gate tests — machine-executable counterpart of the design review checklist (`pytest tests/test_reliability_gates.py -v` or `pytest -m gate -v`); each test class name states the guarantee it protects and fills gaps left by the per-module test files (MoE param split, feature vector parity, schema/predictor agreement, model disk size, notebook FEATURE_COLS parity, training-data disclosure completeness, precision-support enforcement — see each class's own docstring for specifics).""" | |
| from __future__ import annotations | |
| import ast | |
| import json | |
| import math | |
| from pathlib import Path | |
| from typing import get_args | |
| import pandas as pd | |
| import pytest | |
| import yaml | |
| from src.api.schemas import AccuracyTier, Framework, MemoryFitVerdict, RankingObjective, Scenario | |
| from src.data import manifest | |
| from src.data.gpu_spec_db import load_specs | |
| from src.features.build_features import ( | |
| BYTES_PER_PARAM, | |
| MEMORY_OVERHEAD_FACTOR, | |
| MODEL_PARAMS, | |
| TIER_TO_PRECISION, | |
| VALID_MEMORY_FIT_VERDICTS, | |
| _FRAMEWORK_PATTERNS, | |
| _normalize_framework, | |
| build_training_df, | |
| cost_per_million_tokens, | |
| gpu_supports_precision, | |
| memory_fit_verdict, | |
| ) | |
| from src.models.predictor import ( | |
| FEATURE_COLS, | |
| VALID_FRAMEWORKS, | |
| VALID_SCENARIOS, | |
| VALID_TIERS, | |
| GpuPredictor, | |
| _build_feature_vector, | |
| _selected_precision, | |
| ) | |
| from src.recommend.recommender import ( | |
| GpuRecommender, | |
| VALID_RANKING_OBJECTIVES, | |
| _pareto_frontier, | |
| _RANKING_FIELDS, | |
| ) | |
| # Helpers | |
| def _nan_match(a: float, b: float) -> bool: | |
| """True when both are NaN, or both are finite and within 1e-3 of each other.""" | |
| a_nan = math.isnan(a) | |
| b_nan = math.isnan(b) | |
| if a_nan and b_nan: | |
| return True | |
| if a_nan or b_nan: | |
| return False | |
| return abs(a - b) < 1e-3 | |
| def _make_single_row( | |
| gpu_alias: str, | |
| model_name: str, | |
| scenario: str, | |
| tier: str, | |
| raw_framework: str, | |
| throughput: float = 1000.0, | |
| ) -> pd.DataFrame: | |
| """One synthetic MLPerf row with every field build_training_df expects.""" | |
| return pd.DataFrame([{ | |
| "round": "v5.0", | |
| "division": "closed", | |
| "submitter": "test", | |
| "system_name": "test_system", | |
| "gpu_name": gpu_alias, | |
| "num_gpus": 1, | |
| "vram_gb": None, | |
| "framework": raw_framework, | |
| "system_type": "datacenter", | |
| "hw_status": "available", | |
| "benchmark": f"{model_name}-{tier}", | |
| "benchmark_base": model_name, | |
| "benchmark_accuracy_tier": tier, | |
| "scenario": scenario, | |
| "precision": None, | |
| "tokens_per_sample": 294, | |
| "throughput_tokens_per_sec": throughput, | |
| "throughput_tok_per_sec_per_gpu": throughput, | |
| "result_valid": True, | |
| "throughput_samples_per_sec": throughput / 294, | |
| "latency_mean_ms": None, | |
| "latency_p99_ms": None, | |
| "ttft_mean_ms": None, | |
| "ttft_p99_ms": None, | |
| "tpot_mean_ms": None, | |
| "tpot_p99_ms": None, | |
| "log_path": "fake/path", | |
| }]) | |
| # MoE param split — bandwidth ceiling uses total, compute uses active | |
| class TestMoEParamSplitGate: | |
| """GATE: MoE bandwidth ceiling must use total_params_b, compute ceiling must use compute_params_b — swapping these args in roofline_ceilings() would overestimate mixtral-8x7b compute throughput 3.31x (total=46.7B vs active=14.1B) and underestimate bandwidth by the same factor.""" | |
| def mi300x_spec(self) -> dict: | |
| return {s["id"]: s for s in load_specs()}["mi300x"] | |
| def test_mixtral_total_ne_active_params(self) -> None: | |
| total_b, active_b = MODEL_PARAMS["mixtral-8x7b"] | |
| assert total_b != active_b, "mixtral-8x7b must have distinct total and active params" | |
| assert total_b == pytest.approx(46.7, abs=0.1) | |
| assert active_b == pytest.approx(14.1, abs=0.1) | |
| def test_bw_ceiling_uses_total_params(self, mi300x_spec: dict) -> None: | |
| """Back-calculate which param count produced the BW ceiling in the serving path.""" | |
| total_b, active_b = MODEL_PARAMS["mixtral-8x7b"] | |
| precision = TIER_TO_PRECISION["99"] # fp8 | |
| bpp = BYTES_PER_PARAM[precision] | |
| hbm_bw = mi300x_spec["hbm_bandwidth_tbps"] | |
| feats, _, _ = _build_feature_vector( | |
| gpu_spec=mi300x_spec, | |
| model_name="mixtral-8x7b", | |
| scenario="Offline", | |
| accuracy_tier="99", | |
| framework="vllm", | |
| ) | |
| bw_ceil = feats[FEATURE_COLS.index("bandwidth_ceiling_tok_per_sec")] | |
| # bw_ceil = (hbm_bw * 1e12) / (params * 1e9 * bpp) ⟹ params = … | |
| implied_params = (hbm_bw * 1e12) / (bw_ceil * 1e9 * bpp) | |
| assert implied_params == pytest.approx(total_b, rel=1e-3), ( | |
| f"BW ceiling implies {implied_params:.2f}B params in model footprint; " | |
| f"expected total_params_b={total_b}B. " | |
| f"Compute ceiling likely received total params instead of active params." | |
| ) | |
| def test_compute_ceiling_uses_active_params(self, mi300x_spec: dict) -> None: | |
| """Back-calculate which param count produced the compute ceiling in the serving path.""" | |
| total_b, active_b = MODEL_PARAMS["mixtral-8x7b"] | |
| precision = TIER_TO_PRECISION["99"] | |
| pt = mi300x_spec.get("peak_tflops") or {} | |
| peak_tflops = pt.get(precision) or pt.get("fp16") | |
| feats, _, _ = _build_feature_vector( | |
| gpu_spec=mi300x_spec, | |
| model_name="mixtral-8x7b", | |
| scenario="Offline", | |
| accuracy_tier="99", | |
| framework="vllm", | |
| ) | |
| compute_ceil = feats[FEATURE_COLS.index("compute_ceiling_tok_per_sec")] | |
| # compute_ceil = (peak_tflops * 1e12) / (2 * params * 1e9) ⟹ params = … | |
| implied_params = (peak_tflops * 1e12) / (2.0 * compute_ceil * 1e9) | |
| assert implied_params == pytest.approx(active_b, rel=1e-3), ( | |
| f"Compute ceiling implies {implied_params:.2f}B active params; " | |
| f"expected compute_params_b={active_b}B. " | |
| f"Bandwidth ceiling likely received active params instead of total params." | |
| ) | |
| # Memory-fit verdict enforcement — does_not_fit excluded, tight kept | |
| class TestMemoryFitVerdictEnforcementGate: | |
| """GATE: recommend()'s hard memory-fit exclusion must track the tri-state verdict exactly — every 'does_not_fit' lands in `filtered` (never frontier/dominated) and 'tight'/'fits' are never silently dropped; drift (e.g. reusing the old boolean vram_fits check) would recommend an unrunnable config or wrongly exclude a runnable one.""" | |
| def recommender(self) -> GpuRecommender: | |
| return GpuRecommender(GpuPredictor()) | |
| def test_sweep_all_models_verdict_matches_bucket(self, recommender) -> None: | |
| checked_any = False | |
| for model_name in MODEL_PARAMS: | |
| for tier in get_args(AccuracyTier): | |
| # No budget/throughput constraint is passed anywhere in this sweep, so every filtered entry can only be a memory-fit rejection; budget/throughput filtering is covered separately in test_recommender.py. | |
| result = recommender.recommend(model_name=model_name, accuracy_tier=tier) | |
| for f in result["filtered"]: | |
| checked_any = True | |
| assert f["memory_fit_verdict"] == "does_not_fit", ( | |
| f"{model_name}/{tier}/{f['gpu_id']} is filtered with verdict " | |
| f"{f['memory_fit_verdict']!r}, but this sweep sets no budget/throughput " | |
| "constraint, so the only valid filter reason is does_not_fit" | |
| ) | |
| for c in result["frontier"] + result["dominated"]: | |
| checked_any = True | |
| assert c["memory_fit_verdict"] != "does_not_fit", ( | |
| f"{model_name}/{tier}/{c['gpu_id']} is a candidate but " | |
| f"verdict is does_not_fit — should have been excluded" | |
| ) | |
| assert checked_any, "no result entries were produced to check" | |
| # Precision-support enforcement — never silently substituted | |
| class TestPrecisionSupportEnforcementGate: | |
| """GATE: predict()/predict_batch()/recommend() must never return a normal-looking result for a (GPU, tier) whose selected precision has no native peak_tflops entry — before this existed, a100_sxm_80gb@99 (fp8, unsupported on Ampere) silently substituted fp16 TFLOPS for the roofline ceiling while still using fp8 bytes-per-param, surfacing as a real "dominated" recommend() candidate with no error or flag.""" | |
| def predictor(self) -> GpuPredictor: | |
| return GpuPredictor() | |
| def recommender(self, predictor) -> GpuRecommender: | |
| return GpuRecommender(predictor) | |
| def test_predict_raises_iff_precision_unsupported(self, predictor) -> None: | |
| specs = {s["id"]: s for s in load_specs() if s.get("in_model_scope")} | |
| checked_any = False | |
| checked_unsupported = False | |
| for gpu_id, spec in specs.items(): | |
| for tier in get_args(AccuracyTier): | |
| selected = _selected_precision(spec, tier) | |
| supported = gpu_supports_precision(spec, selected) | |
| checked_any = True | |
| if supported: | |
| result = predictor.predict( | |
| gpu_id=gpu_id, model_name="gptj", accuracy_tier=tier | |
| ) | |
| assert result["pred_throughput_tok_per_sec"] >= 0.0, ( | |
| f"{gpu_id}/{tier} (selected={selected}, supported) " | |
| "should predict normally" | |
| ) | |
| else: | |
| checked_unsupported = True | |
| with pytest.raises(ValueError, match="does not support precision"): | |
| predictor.predict( | |
| gpu_id=gpu_id, model_name="gptj", accuracy_tier=tier | |
| ) | |
| assert checked_any, "no (gpu, tier) combos were checked" | |
| assert checked_unsupported, ( | |
| "no unsupported (gpu, tier) combo was exercised — the gate " | |
| "would pass vacuously if gpu_specs.yaml ever dropped its one " | |
| "unsupported-precision case (a100_sxm_80gb.peak_tflops.fp8)" | |
| ) | |
| def test_recommend_never_returns_unsupported_precision_as_candidate( | |
| self, recommender | |
| ) -> None: | |
| specs = {s["id"]: s for s in load_specs() if s.get("in_model_scope")} | |
| checked_any = False | |
| checked_unsupported = False | |
| for model_name in MODEL_PARAMS: | |
| for tier in get_args(AccuracyTier): | |
| result = recommender.recommend(model_name=model_name, accuracy_tier=tier) | |
| for c in result["frontier"] + result["dominated"]: | |
| checked_any = True | |
| spec = specs[c["gpu_id"]] | |
| selected = _selected_precision(spec, tier) | |
| assert gpu_supports_precision(spec, selected), ( | |
| f"{model_name}/{tier}/{c['gpu_id']} is a candidate " | |
| f"but does not support its selected precision {selected!r}" | |
| ) | |
| for f in result["filtered"]: | |
| checked_any = True | |
| spec = specs[f["gpu_id"]] | |
| selected = _selected_precision(spec, tier) | |
| if not gpu_supports_precision(spec, selected): | |
| checked_unsupported = True | |
| assert selected in f["reject_reason"], ( | |
| f"{model_name}/{tier}/{f['gpu_id']} filtered for " | |
| f"an unsupported precision, but reject_reason " | |
| f"{f['reject_reason']!r} doesn't name it" | |
| ) | |
| assert checked_any, "no result entries were produced to check" | |
| assert checked_unsupported, ( | |
| "no unsupported-precision filtered entry was exercised across " | |
| "the full model x tier sweep" | |
| ) | |
| # Schema/predictor agreement — Literal types match VALID_* frozensets | |
| class TestSchemaPredictorAgreementGate: | |
| """GATE: Pydantic Literal types in src/api/schemas.py must exactly match the independently-defined VALID_* frozensets in src/models/predictor.py — schemas accepting more causes a 500 at the predictor, schemas accepting less makes a valid predictor call unreachable via the API.""" | |
| def test_scenario_literals_match_valid_scenarios(self) -> None: | |
| assert set(get_args(Scenario)) == VALID_SCENARIOS, ( | |
| f"schemas.Scenario={set(get_args(Scenario))} " | |
| f"!= predictor.VALID_SCENARIOS={VALID_SCENARIOS}" | |
| ) | |
| def test_accuracy_tier_literals_match_valid_tiers(self) -> None: | |
| assert set(get_args(AccuracyTier)) == VALID_TIERS, ( | |
| f"schemas.AccuracyTier={set(get_args(AccuracyTier))} " | |
| f"!= predictor.VALID_TIERS={VALID_TIERS}" | |
| ) | |
| def test_framework_literals_match_valid_frameworks(self) -> None: | |
| assert set(get_args(Framework)) == VALID_FRAMEWORKS, ( | |
| f"schemas.Framework={set(get_args(Framework))} " | |
| f"!= predictor.VALID_FRAMEWORKS={VALID_FRAMEWORKS}" | |
| ) | |
| # has_training_data completeness across every recommend() code path | |
| class TestHasTrainingDataCompletenessGate: | |
| """GATE: every dict recommend() returns (frontier/dominated/filtered) must carry 'has_training_data' and 'training_data_tier' regardless of which of the three independent code paths built it (predict_batch-derived, VRAM-fail reject, precision-fail reject) — a future path that forgets either field would pass unit tests then 500 at request time on real traffic; both fields are checked together since they're added/read at the same call sites.""" | |
| def predictor(self) -> GpuPredictor: | |
| return GpuPredictor() | |
| def recommender(self, predictor: GpuPredictor) -> GpuRecommender: | |
| return GpuRecommender(predictor) | |
| def test_zero_row_gpus_flagged_false(self, predictor: GpuPredictor) -> None: | |
| # a100_sxm_80gb/l4/rtx4090 are in_model_scope with zero training rows — a regression here means an unvalidated prediction is silently presented with the same confidence as a real one. | |
| for gpu_id in ("a100_sxm_80gb", "l4", "rtx4090"): | |
| assert predictor.has_training_data(gpu_id) is False, ( | |
| f"{gpu_id} unexpectedly has training data — " | |
| "if this is intentional (new calibration data added), " | |
| "update this gate and the disclosure copy in the UI/README." | |
| ) | |
| assert predictor.training_data_tier(gpu_id) == "none", ( | |
| f"{gpu_id} unexpectedly has a non-'none' training_data_tier." | |
| ) | |
| def test_amd_gpus_below_the_reliability_floor_flagged(self, predictor: GpuPredictor) -> None: | |
| # mi300x/mi325x/mi355x have real rows but fewer than the 100-row-per-GPU floor — the exact case a plain boolean silently conflated with "sufficient" until this gate/field existed. | |
| for gpu_id in ("mi300x", "mi325x", "mi355x"): | |
| assert predictor.has_training_data(gpu_id) is True, ( | |
| f"{gpu_id} unexpectedly has zero training rows — " | |
| "if the corpus shrank, update this gate." | |
| ) | |
| assert predictor.training_data_tier(gpu_id) == "below_floor", ( | |
| f"{gpu_id}'s training_data_tier is no longer 'below_floor' — " | |
| "if this GPU's row count crossed 100 (e.g. new calibration data), " | |
| "that's a real improvement — update this gate and the disclosure " | |
| "copy in the UI/README rather than silently letting it drift." | |
| ) | |
| def test_every_result_dict_carries_both_fields(self, recommender: GpuRecommender) -> None: | |
| # Exercises all three branches in one pass: llama3.1-405b guarantees VRAM-fail entries (nothing fits 405B); a $1/hr budget guarantees budget-reject entries plus a populated frontier/dominated. | |
| combos = [ | |
| dict(model_name="llama3.1-405b", accuracy_tier="99.9"), | |
| dict(model_name="gptj", accuracy_tier="99", budget_per_gpu_hr=1.0), | |
| ] | |
| checked_any = False | |
| for kwargs in combos: | |
| result = recommender.recommend(**kwargs) | |
| for bucket in ("frontier", "dominated", "filtered"): | |
| for entry in result[bucket]: | |
| checked_any = True | |
| assert "has_training_data" in entry, ( | |
| f"{bucket} entry for {entry.get('gpu_id')} " | |
| f"(query {kwargs}) is missing has_training_data — " | |
| "this would fail Pydantic response validation at " | |
| "request time, not at test time." | |
| ) | |
| assert "training_data_tier" in entry, ( | |
| f"{bucket} entry for {entry.get('gpu_id')} " | |
| f"(query {kwargs}) is missing training_data_tier — " | |
| "this would fail Pydantic response validation at " | |
| "request time, not at test time." | |
| ) | |
| assert entry["training_data_tier"] in ("none", "below_floor", "sufficient"), ( | |
| f"{bucket} entry for {entry.get('gpu_id')} has an " | |
| f"invalid training_data_tier: {entry['training_data_tier']!r}" | |
| ) | |
| assert checked_any, ( | |
| "no result entries were produced to check — queries need adjusting" | |
| ) | |
| # Feature vector parity — training path vs serving path | |
| class TestFeatureVectorParityGate: | |
| """GATE: all 20 FEATURE_COLS values match between build_training_df (training path) and _build_feature_vector (serving path) — 13 continuous features here, 6 categorical in test_predictor.py::TestBuildFeatureVector, and mlperf_round_num intentionally excluded (training uses the real round ordinal, serving always uses the latest) and verified separately there.""" | |
| # (gpu_alias, gpu_id, model_name, scenario, tier, raw_framework); gpu_alias must match a known alias in data/gpu_specs.yaml, raw_framework is a realistic MLPerf string that _normalize_framework maps to the family label _build_feature_vector expects. | |
| CASES = [ | |
| # NVIDIA Hopper, FP8 tier (99 -> fp8, falls back for GPUs without fp8) | |
| ( | |
| "NVIDIA H200-SXM-141GB", "h200_sxm", "llama2-70b", | |
| "Offline", "99", "TensorRT 10.2.0", | |
| ), | |
| # AMD CDNA3, FP8 tier (99.9 -> fp8 for AMD) — verifies both paths apply the vendor override consistently. | |
| ( | |
| "AMD Instinct MI300X 192GB HBM3", "mi300x", "gptj", | |
| "Server", "99.9", "vLLM 0.4.3+rocm614", | |
| ), | |
| # AMD CDNA3, BF16 tier (base -> bf16); MoE model (active != total params) | |
| ( | |
| "AMD Instinct MI325X", "mi325x", "mixtral-8x7b", | |
| "Offline", "99", "ROCm 7.0", | |
| ), | |
| # AMD CDNA4 — only GPU where amd_arch_gen=2 and is_cdna4=1; a CDNA4 arch-ordinal encoding divergence can't be detected by the 3 CDNA3/NVIDIA cases above. | |
| ( | |
| "AMD Instinct MI355X 288GB HBM3e", "mi355x", "llama2-70b", | |
| "Offline", "99", "ROCm 7.0", | |
| ), | |
| # NVIDIA at tier 99.9 selects fp16 TFLOPS (not fp8, no AMD override) — without this case a training/serving divergence in NVIDIA fp16 TFLOPS selection would pass undetected (all other cases are tier 99 or AMD 99.9, both fp8). | |
| ( | |
| "NVIDIA H100-SXM-80GB", "h100_sxm", "gptj", | |
| "Server", "99.9", "TensorRT 10.4.0", | |
| ), | |
| ] | |
| def spec_map(self) -> dict: | |
| return {s["id"]: s for s in load_specs()} | |
| def test_continuous_features_match( | |
| self, spec_map, gpu_alias, gpu_id, model_name, scenario, tier, raw_fw, | |
| ): | |
| """GATE: every continuous FEATURE_COL has the same value in both paths — a mismatch means the model evaluates features at serving time that it was never trained on.""" | |
| # ── Training path ───────────────────────────────────────────────── | |
| raw_df = _make_single_row(gpu_alias, model_name, scenario, tier, raw_fw) | |
| feat_df = build_training_df(raw_df) | |
| assert len(feat_df) == 1, ( | |
| f"build_training_df dropped the row — check gpu_alias={gpu_alias!r} " | |
| f"and benchmark_base={model_name!r}" | |
| ) | |
| train_row = feat_df.iloc[0] | |
| # ── Serving path ────────────────────────────────────────────────── | |
| fw_family = _normalize_framework(raw_fw) | |
| serving_feats, _, _ = _build_feature_vector( | |
| gpu_spec=spec_map[gpu_id], | |
| model_name=model_name, | |
| scenario=scenario, | |
| accuracy_tier=tier, | |
| framework=fw_family, | |
| ) | |
| # ── Compare continuous columns — mlperf_round_num excluded, its training/serving divergence is intentional, see class docstring. | |
| continuous_cols = [ | |
| c for c in FEATURE_COLS | |
| if c not in { | |
| "scenario_offline", "is_base_tier", | |
| "fw_tensorrt", "fw_vllm", "fw_rocm_other", "is_cdna4", | |
| "mlperf_round_num", | |
| } | |
| ] | |
| mismatches = [] | |
| for col in continuous_cols: | |
| idx = FEATURE_COLS.index(col) | |
| sv = float(serving_feats[idx]) | |
| tv_raw = train_row[col] | |
| tv = float(tv_raw) if not pd.isna(tv_raw) else float("nan") | |
| if not _nan_match(sv, tv): | |
| mismatches.append( | |
| f" {col:<40} serving={sv:.6g} training={tv:.6g}" | |
| ) | |
| assert not mismatches, ( | |
| f"feature mismatch for gpu={gpu_id!r} model={model_name!r} " | |
| f"scenario={scenario!r} tier={tier!r}:\n" | |
| + "\n".join(mismatches) | |
| ) | |
| # Model artifact total disk size < 50 MB | |
| _MODEL_DIR = Path("data/models") | |
| _DISK_LIMIT_MB = 50 | |
| class TestModelDiskSizeGate: | |
| """GATE: data/models/ total size is < 50 MB — the deployment budget for free-tier Hugging Face Spaces (shared storage, slow cold-start I/O).""" | |
| def test_model_dir_exists(self) -> None: | |
| assert _MODEL_DIR.is_dir(), ( | |
| f"{_MODEL_DIR} not found — run train_final.py to generate artifacts" | |
| ) | |
| def test_total_size_under_limit(self) -> None: | |
| total_bytes = sum(f.stat().st_size for f in _MODEL_DIR.rglob("*") if f.is_file()) | |
| total_mb = total_bytes / (1024 ** 2) | |
| assert total_mb < _DISK_LIMIT_MB, ( | |
| f"data/models/ is {total_mb:.1f} MB — exceeds {_DISK_LIMIT_MB} MB limit. " | |
| "Trim model artifacts before deploying to HF Spaces." | |
| ) | |
| def test_required_artifacts_present(self) -> None: | |
| required = ["prophet_v1.json", "feature_metadata.json"] | |
| for name in required: | |
| assert (_MODEL_DIR / name).exists(), ( | |
| f"required artifact {name} is missing from {_MODEL_DIR}" | |
| ) | |
| # Pricing coverage — all in-scope GPUs have pricing entries | |
| _PRICING_PATH = Path("data/pricing.yaml") | |
| class TestPricingCoverageGate: | |
| """GATE: every in_model_scope=True GPU has a data/pricing.yaml entry — GpuRecommender.__init__()'s ValueError for missing pricing is only visible at startup, and a gap would otherwise cause cost_efficiency=None, raising TypeError in _pareto_frontier's sort and silently breaking all recommendation responses.""" | |
| def test_pricing_file_exists(self) -> None: | |
| assert _PRICING_PATH.is_file(), ( | |
| f"{_PRICING_PATH} not found — create data/pricing.yaml before deployment" | |
| ) | |
| def test_all_in_scope_gpus_have_pricing(self) -> None: | |
| with _PRICING_PATH.open() as f: | |
| pricing: dict = yaml.safe_load(f).get("pricing", {}) | |
| in_scope_ids = [s["id"] for s in load_specs() if s.get("in_model_scope")] | |
| assert in_scope_ids, ( | |
| "no in-scope GPUs found — check gpu_specs.yaml in_model_scope flags" | |
| ) | |
| missing = [gid for gid in in_scope_ids if gid not in pricing] | |
| assert not missing, ( | |
| f"pricing.yaml missing entries for in-scope GPUs: {missing}. " | |
| "GpuRecommender.__init__() will raise ValueError at startup." | |
| ) | |
| # AMD FP8 override — three-way consistency gate | |
| class TestAmdFp8OverrideGate: | |
| """GATE: the AMD 99.9-tier FP8 override must apply consistently across build_training_df (training), _build_feature_vector (serving), and _gpu_model_size_gb (VRAM pre-filter) — a regression skews MI355X training efficiency_ratio or wrongly rejects MI300X for llama2-70b@99.9 (140 GB FP16 vs 192 GB VRAM) despite fitting as 70 GB FP8; verified end-to-end via vram_headroom ≈0.635 (FP8) vs ≈0.271 (FP16, drifted).""" | |
| def recommender(self) -> GpuRecommender: | |
| return GpuRecommender(GpuPredictor()) | |
| def test_mi300x_vram_headroom_uses_fp8_not_fp16(self, recommender) -> None: | |
| # Minimal batch/context override isolates the FP8-vs-FP16 weight comparison this gate targets from the KV-cache term (covered separately by test_recommender.py). | |
| result = recommender.recommend( | |
| model_name="llama2-70b", accuracy_tier="99.9", | |
| batch_size=1, input_tokens=64, output_tokens=1, | |
| ) | |
| candidates = result["frontier"] + result["dominated"] | |
| mi300x = next((r for r in candidates if r["gpu_id"] == "mi300x"), None) | |
| assert mi300x is not None, ( | |
| "MI300X absent from llama2-70b tier-99.9 candidates — " | |
| "VRAM filter may be applying FP16 model size (140 GB) instead of FP8 (70 GB), " | |
| "causing MI300X (192 GB) to be incorrectly rejected." | |
| ) | |
| fp8_model_gb = 70.0 # 70B params × 1 byte/param (FP8) | |
| expected_headroom = (192.0 - fp8_model_gb * 1.10) / 192.0 # + 10% overhead, ≈ 0.599 | |
| assert mi300x["vram_headroom"] == pytest.approx(expected_headroom, abs=0.01), ( | |
| f"MI300X vram_headroom={mi300x['vram_headroom']:.3f}, " | |
| f"expected {expected_headroom:.3f} (FP8 70 GB model). " | |
| "A value near 0.271 indicates FP16 (140 GB) is being used instead." | |
| ) | |
| # Framework normalization — pattern outputs subset of VALID_FRAMEWORKS | |
| class TestFrameworkNormalizationGate: | |
| """GATE: every _FRAMEWORK_PATTERNS label (and the "other" fallback) is in VALID_FRAMEWORKS — a pattern added with a label outside VALID_FRAMEWORKS would enter training data but never be requestable via the API, causing silent 500s (predictor._validate() rejecting an apparently valid input).""" | |
| def test_all_pattern_outputs_in_valid_frameworks(self) -> None: | |
| labels_from_patterns = {label for _, label in _FRAMEWORK_PATTERNS} | |
| unknown_labels = labels_from_patterns - VALID_FRAMEWORKS | |
| assert not unknown_labels, ( | |
| f"_FRAMEWORK_PATTERNS returns labels not in VALID_FRAMEWORKS: " | |
| f"{unknown_labels}. Add to predictor.VALID_FRAMEWORKS or remove from patterns." | |
| ) | |
| def test_fallback_other_in_valid_frameworks(self) -> None: | |
| fallback = _normalize_framework("some_completely_unknown_framework_xyz") | |
| assert fallback == "other", ( | |
| f"no-match fallback should be 'other', got {fallback!r}. " | |
| "The fallback must be in VALID_FRAMEWORKS so unrecognized frameworks " | |
| "can be requested via the API without a 422 validation error." | |
| ) | |
| assert "other" in VALID_FRAMEWORKS, ( | |
| "'other' is _normalize_framework's no-match return value " | |
| "but is not in VALID_FRAMEWORKS — unrecognized frameworks fail predictor._validate()." | |
| ) | |
| # Notebook FEATURE_COLS parity — training/serving vs. analysis notebooks | |
| _NOTEBOOKS_DIR = Path("notebooks") | |
| _FEATURE_COLS_NOTEBOOKS: tuple[str, ...] = ( | |
| "03_model_training.ipynb", | |
| "04_top1_benchmark.ipynb", | |
| "05_mr008_ablation.ipynb", | |
| ) | |
| # In-place mutations of FEATURE_COLS (or a list it's built from) can't be verified statically, so the value this module resolves may not match what the notebook computes at runtime. | |
| _MUTATING_LIST_METHODS = frozenset({ | |
| "append", "extend", "insert", "remove", "pop", "clear", "sort", "reverse", | |
| }) | |
| def _parse_notebook_feature_cols(notebook_path: Path) -> list[str]: | |
| """Extract the notebook's actual FEATURE_COLS = X + Y + ... expression via AST (without executing the notebook) — order matters, not just the set, since XGBoost's colsample_bytree does positional random feature subsampling.""" | |
| nb = json.loads(notebook_path.read_text()) | |
| code = "\n".join( | |
| "".join(cell["source"]) | |
| for cell in nb["cells"] | |
| if cell["cell_type"] == "code" | |
| ) | |
| tree = ast.parse(code) | |
| list_literals: dict[str, list[str]] = {} | |
| feature_cols_expr: ast.expr | None = None | |
| for node in ast.walk(tree): | |
| if isinstance(node, ast.Assign): | |
| for target in node.targets: | |
| if not isinstance(target, ast.Name): | |
| continue | |
| if isinstance(node.value, ast.List): | |
| list_literals[target.id] = [ | |
| elt.value for elt in node.value.elts | |
| if isinstance(elt, ast.Constant) and isinstance(elt.value, str) | |
| ] | |
| elif target.id == "FEATURE_COLS": | |
| feature_cols_expr = node.value | |
| if feature_cols_expr is None: | |
| raise RuntimeError(f"FEATURE_COLS assignment not found in {notebook_path}") | |
| # Names this resolution depends on, populated by _resolve below; FEATURE_COLS is always included so a bare `FEATURE_COLS += [...]` after the found assignment is still caught. | |
| referenced_names: set[str] = {"FEATURE_COLS"} | |
| def _resolve(expr: ast.expr) -> list[str]: | |
| if isinstance(expr, ast.Name): | |
| referenced_names.add(expr.id) | |
| if expr.id not in list_literals: | |
| raise RuntimeError(f"{expr.id} list literal not found in {notebook_path}") | |
| return list_literals[expr.id] | |
| if isinstance(expr, ast.BinOp) and isinstance(expr.op, ast.Add): | |
| return _resolve(expr.left) + _resolve(expr.right) | |
| raise RuntimeError( | |
| f"Unsupported FEATURE_COLS expression in {notebook_path}: {ast.dump(expr)}" | |
| ) | |
| result = _resolve(feature_cols_expr) | |
| # The scan above only sees each name's initial literal assignment, not later in-place mutation (`+=`, `.append(...)`, etc) — since that's exactly the drift this gate exists to catch, fail loudly rather than silently trust a possibly-stale value. | |
| for node in ast.walk(tree): | |
| if isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name): | |
| if node.target.id in referenced_names: | |
| raise RuntimeError( | |
| f"{notebook_path} mutates {node.target.id!r} with '+=' — " | |
| "static AST parsing can't verify the resulting FEATURE_COLS. " | |
| "Rewrite as a single 'FEATURE_COLS = A + B + ...' assignment." | |
| ) | |
| if ( | |
| isinstance(node, ast.Call) | |
| and isinstance(node.func, ast.Attribute) | |
| and node.func.attr in _MUTATING_LIST_METHODS | |
| and isinstance(node.func.value, ast.Name) | |
| and node.func.value.id in referenced_names | |
| ): | |
| raise RuntimeError( | |
| f"{notebook_path} calls " | |
| f"{node.func.value.id}.{node.func.attr}(...) — static AST parsing " | |
| "can't verify the resulting FEATURE_COLS. Rewrite as a single " | |
| "'FEATURE_COLS = A + B + ...' assignment." | |
| ) | |
| return result | |
| class TestNotebookFeatureColsParityGate: | |
| """GATE: every notebook manually reconstructing the feature list must match predictor.FEATURE_COLS exactly, in order — TestFeatureVectorParityGate protects the two source-code copies, but only this gate catches the two notebooks silently drifting out of sync after a FEATURE_COLS change (otherwise only caught by manually re-running them).""" | |
| def test_notebook_feature_cols_match_predictor(self, notebook_name: str) -> None: | |
| notebook_path = _NOTEBOOKS_DIR / notebook_name | |
| assert notebook_path.is_file(), f"{notebook_path} not found" | |
| notebook_cols = _parse_notebook_feature_cols(notebook_path) | |
| assert notebook_cols == list(FEATURE_COLS), ( | |
| f"{notebook_name} FEATURE_COLS diverges from predictor.FEATURE_COLS.\n" | |
| f" notebook: {notebook_cols}\n" | |
| f" predictor: {list(FEATURE_COLS)}" | |
| ) | |
| # memory_fit_verdict() output subset of VALID_MEMORY_FIT_VERDICTS | |
| class TestMemoryFitVerdictLabelsGate: | |
| """GATE: memory_fit_verdict() only ever returns a value from VALID_MEMORY_FIT_VERDICTS, and schemas.MemoryFitVerdict matches that set exactly — mirrors the framework-label closed-set gate; an unlisted fourth verdict from a future threshold-logic change would otherwise silently 500 at request time via Pydantic validation instead of failing a test.""" | |
| def test_verdict_always_in_valid_set(self, utilization_target: float) -> None: | |
| vram_gb = 100.0 | |
| weights_gb = utilization_target * vram_gb / MEMORY_OVERHEAD_FACTOR | |
| verdict, _, _ = memory_fit_verdict(weights_gb, 0.0, vram_gb) | |
| assert verdict in VALID_MEMORY_FIT_VERDICTS, ( | |
| f"memory_fit_verdict() returned {verdict!r}, " | |
| f"not in VALID_MEMORY_FIT_VERDICTS={VALID_MEMORY_FIT_VERDICTS}" | |
| ) | |
| def test_schema_literal_matches_valid_set(self) -> None: | |
| assert set(get_args(MemoryFitVerdict)) == VALID_MEMORY_FIT_VERDICTS, ( | |
| f"schemas.MemoryFitVerdict={set(get_args(MemoryFitVerdict))} " | |
| f"!= build_features.VALID_MEMORY_FIT_VERDICTS={VALID_MEMORY_FIT_VERDICTS}" | |
| ) | |
| # RecommendRequest.ranking_objective subset of VALID_RANKING_OBJECTIVES | |
| class TestRankingObjectiveLabelsGate: | |
| """GATE: schemas.RankingObjective matches recommender.VALID_RANKING_OBJECTIVES exactly, and _RANKING_FIELDS covers every member — mirrors the memory-fit-verdict/framework-label closed-set gates; a future typo in either set would silently diverge (accepted by one, KeyError/422 from the other) with no test failing.""" | |
| def test_schema_literal_matches_valid_set(self) -> None: | |
| assert set(get_args(RankingObjective)) == VALID_RANKING_OBJECTIVES, ( | |
| f"schemas.RankingObjective={set(get_args(RankingObjective))} " | |
| f"!= recommender.VALID_RANKING_OBJECTIVES={VALID_RANKING_OBJECTIVES}" | |
| ) | |
| def test_every_objective_is_a_real_recommender_ranking_field(self) -> None: | |
| assert set(_RANKING_FIELDS) == VALID_RANKING_OBJECTIVES, ( | |
| f"recommender._RANKING_FIELDS keys={set(_RANKING_FIELDS)} " | |
| f"!= VALID_RANKING_OBJECTIVES={VALID_RANKING_OBJECTIVES}" | |
| ) | |
| def test_every_objective_accepted_by_recommend(self, objective: str) -> None: | |
| # Full round-trip through the public entry point, not just the internal frozenset — catches a rejection at the recommend() validation layer even if the frozenset/Literal agree. | |
| predictor = GpuPredictor() | |
| recommender = GpuRecommender(predictor) | |
| result = recommender.recommend(model_name="gptj", ranking_objective=objective) | |
| assert result["workload"]["ranking_objective"] == objective | |
| # Pareto objective vector uses (throughput, price, watts), not the earlier (throughput, cost_efficiency, vram_headroom) | |
| class TestParetoObjectiveVectorGate: | |
| """GATE: _pareto_frontier()'s dominance check must read watts/price_per_gpu_hr directly — a pair differing only in watts must change the outcome, and one differing only in vram_headroom must NOT, proving the spec's (throughput, usd_per_hour, watts) vector, guarding against silently backsliding to the old (throughput, cost_efficiency, vram_headroom) vector.""" | |
| def _cand( | |
| gpu_id: str, throughput: float, price: float, watts: float, vram_headroom: float, | |
| ) -> dict: | |
| return { | |
| "gpu_id": gpu_id, | |
| "throughput": throughput, | |
| "price_per_gpu_hr": price, | |
| "watts": watts, | |
| "vram_headroom": vram_headroom, | |
| "cost_efficiency": throughput / price, | |
| "tokens_per_watt": throughput / watts, | |
| "cost_per_million_tokens": cost_per_million_tokens(price, throughput), | |
| } | |
| def test_watts_affects_dominance(self) -> None: | |
| # Identical throughput/price/vram_headroom; B draws less power than A. | |
| a = self._cand("a", 1000.0, 2.0, 1000.0, 0.5) | |
| b = self._cand("b", 1000.0, 2.0, 100.0, 0.5) | |
| frontier, dominated = _pareto_frontier([a, b]) | |
| assert [c["gpu_id"] for c in frontier] == ["b"], ( | |
| "lower watts at identical throughput/price must dominate — " | |
| "watts is not affecting Pareto dominance" | |
| ) | |
| assert [c["gpu_id"] for c in dominated] == ["a"] | |
| def test_vram_headroom_does_not_affect_dominance(self) -> None: | |
| # Identical throughput/price/watts, only vram_headroom differs — the objective vector has no room for a fourth soft axis, so neither may dominate on vram_headroom alone. | |
| a = self._cand("a", 1000.0, 2.0, 500.0, 0.9) | |
| b = self._cand("b", 1000.0, 2.0, 500.0, 0.1) | |
| frontier, dominated = _pareto_frontier([a, b]) | |
| assert {c["gpu_id"] for c in frontier} == {"a", "b"}, ( | |
| "vram_headroom must not affect Pareto dominance — " | |
| f"got frontier={[c['gpu_id'] for c in frontier]}, " | |
| f"dominated={[c['gpu_id'] for c in dominated]}" | |
| ) | |
| assert dominated == [] | |
| # Calibration runner/merge script CSV compatibility | |
| _RUNNER_PATH = Path("benchmarks/run_mi300x_calibration.py") | |
| # Fields merge_calibration_rows.py accesses directly from CSV rows (r["field"] or r.get(...)); update whenever the merge script adds a new field access. | |
| _MERGE_REQUIRED_FIELDS: frozenset[str] = frozenset({ | |
| "round_tag", | |
| "gpu_name", | |
| "benchmark_base", | |
| "benchmark_accuracy_tier", | |
| "scenario", | |
| "throughput_tok_per_sec", | |
| "vllm_version", | |
| # DataFrame subscript access (merge_calibration_rows.py L64) is not a .get() — raises KeyError if the column is absent from the CSV. | |
| "precision_used", | |
| }) | |
| def _parse_runner_csv_fields() -> frozenset[str]: | |
| """Extract CSV_FIELDS list from runner script via AST — avoids importing the script directly, which would fail without vllm installed.""" | |
| tree = ast.parse(_RUNNER_PATH.read_text()) | |
| for node in ast.walk(tree): | |
| if isinstance(node, ast.Assign): | |
| for target in node.targets: | |
| if isinstance(target, ast.Name) and target.id == "CSV_FIELDS": | |
| if isinstance(node.value, ast.List): | |
| return frozenset( | |
| elt.value | |
| for elt in node.value.elts | |
| if isinstance(elt, ast.Constant) and isinstance(elt.value, str) | |
| ) | |
| raise RuntimeError("CSV_FIELDS constant not found in benchmarks/run_mi300x_calibration.py") | |
| class TestRunnerMergeCompatibilityGate: | |
| """GATE: run_mi300x_calibration.py's CSV_FIELDS is a superset of the columns merge_calibration_rows.py reads — drift causes KeyError (or silent None) when processing real AMD Dev Cloud results after the 25-hr credit clock is already spent, so this gate runs locally before the timed session starts.""" | |
| def test_runner_script_exists(self) -> None: | |
| assert _RUNNER_PATH.is_file(), ( | |
| f"{_RUNNER_PATH} not found — calibration runner script is missing" | |
| ) | |
| def test_runner_csv_fields_cover_merge_required(self) -> None: | |
| runner_fields = _parse_runner_csv_fields() | |
| missing = _MERGE_REQUIRED_FIELDS - runner_fields | |
| assert not missing, ( | |
| f"merge_calibration_rows.py accesses fields not in runner CSV_FIELDS: " | |
| f"{missing}. Add these to CSV_FIELDS in run_mi300x_calibration.py, or " | |
| "remove the access from merge_calibration_rows.py." | |
| ) | |
| class TestDataManifestLockFreshnessGate: | |
| """GATE: data_manifest.lock's committed calibration-CSV entry matches the CSV's real SHA-256 — the only manifest entry checkable in CI this way, since the gitignored MLPerf mirrors need a live local checkout (checked instead by train_final.py's warn-only verify_manifest()); this gate catches the one drift class reproducible from a fresh clone: the lock going stale against its own tracked sibling.""" | |
| def test_manifest_lock_file_exists(self) -> None: | |
| assert manifest.MANIFEST_PATH.is_file(), ( | |
| f"{manifest.MANIFEST_PATH} not found — run " | |
| "`python -m src.data.manifest` to generate it." | |
| ) | |
| def test_calibration_entry_matches_tracked_csv(self) -> None: | |
| locked = manifest.load_manifest() | |
| assert locked is not None, "data_manifest.lock failed to load" | |
| entry = locked["sources"].get("mi300x_calibration") | |
| assert entry is not None, "data_manifest.lock missing 'mi300x_calibration' entry" | |
| current_sha256 = manifest._file_sha256(manifest.CALIBRATION_CSV) | |
| assert entry["sha256"] == current_sha256, ( | |
| "data_manifest.lock's mi300x_calibration sha256 is stale — " | |
| "the tracked CSV changed since the lock file was last regenerated. " | |
| "Run `python -m src.data.manifest` and commit the refreshed lock file." | |
| ) | |
| # feature_metadata.json carries a well-formed corpus_sha256 | |
| _FEATURE_METADATA_PATH = Path("data/models/feature_metadata.json") | |
| _TRAINING_CORPUS_PATH = Path("data/processed/mlperf_features.parquet") | |
| _SHA256_HEX_CHARS = frozenset("0123456789abcdef") | |
| class TestCorpusShaSidecarGate: | |
| """GATE: feature_metadata.json records a well-formed corpus_sha256, completing the lineage chain served prediction -> feature_metadata.json -> data_manifest.lock -> pinned MLPerf commits; the field-presence/shape check always runs (metadata is committed), but the byte-for-byte match against the live corpus is best-effort since data/processed/ is gitignored and usually absent in CI.""" | |
| def test_feature_metadata_exists(self) -> None: | |
| assert _FEATURE_METADATA_PATH.is_file(), ( | |
| f"{_FEATURE_METADATA_PATH} not found — run train_final.py " | |
| "to generate artifacts" | |
| ) | |
| def test_corpus_sha256_present_and_well_formed(self) -> None: | |
| meta = json.loads(_FEATURE_METADATA_PATH.read_text()) | |
| corpus_hash = meta.get("corpus_sha256") | |
| assert corpus_hash, ( | |
| "feature_metadata.json missing 'corpus_sha256' — retrain " | |
| "with `python -m src.models.train_final` to populate it." | |
| ) | |
| assert len(corpus_hash) == 64 and set(corpus_hash) <= _SHA256_HEX_CHARS, ( | |
| f"corpus_sha256 {corpus_hash!r} is not a well-formed SHA-256 hex digest" | |
| ) | |
| def test_corpus_sha256_matches_live_corpus_when_present(self) -> None: | |
| if not _TRAINING_CORPUS_PATH.is_file(): | |
| pytest.skip( | |
| f"{_TRAINING_CORPUS_PATH} not present locally (gitignored, " | |
| "not fetched/built) — nothing to compare against." | |
| ) | |
| meta = json.loads(_FEATURE_METADATA_PATH.read_text()) | |
| current_hash = manifest.corpus_sha256(_TRAINING_CORPUS_PATH) | |
| assert meta.get("corpus_sha256") == current_hash, ( | |
| "feature_metadata.json's corpus_sha256 is stale — the training " | |
| "corpus changed since the model was last trained. Run " | |
| "`python -m src.models.train_final` and commit the refreshed artifacts." | |
| ) | |
| def test_manifest_verified_present_and_boolean(self) -> None: | |
| """corpus_sha256 alone proves only *which* corpus trained the model, not whether its raw sources matched data_manifest.lock at build time — manifest_verified records that check's outcome (train_final.py's verify_manifest(), which only logs a warning and would otherwise leave no trace).""" | |
| meta = json.loads(_FEATURE_METADATA_PATH.read_text()) | |
| assert "manifest_verified" in meta, ( | |
| "feature_metadata.json missing 'manifest_verified' — retrain " | |
| "with `python -m src.models.train_final` to populate it." | |
| ) | |
| assert isinstance(meta["manifest_verified"], bool), ( | |
| f"manifest_verified should be a bool, got {type(meta['manifest_verified'])!r}" | |
| ) | |
| def test_manifest_sources_coverage_present_and_sane(self) -> None: | |
| """manifest_verified alone can't distinguish "every source present and matched" from "every source simply absent, trivially not mismatched" — manifest_sources_present/total give the denominator needed to judge what manifest_verified: true is actually worth for this build.""" | |
| meta = json.loads(_FEATURE_METADATA_PATH.read_text()) | |
| assert "manifest_sources_present" in meta and "manifest_sources_total" in meta, ( | |
| "feature_metadata.json missing manifest_sources_present/total — " | |
| "retrain with `python -m src.models.train_final` to populate it." | |
| ) | |
| present, total = meta["manifest_sources_present"], meta["manifest_sources_total"] | |
| assert isinstance(present, int) and isinstance(total, int), ( | |
| "manifest_sources_present/total should be ints" | |
| ) | |
| assert 0 <= present <= total, ( | |
| f"manifest_sources_present ({present}) must be between 0 and " | |
| f"manifest_sources_total ({total})" | |
| ) | |
| def test_validation_metrics_present_and_well_formed(self) -> None: | |
| """The key must always exist (possibly null, if notebooks/03 hasn't been re-run since the last corpus build); when non-null, its primary summary fields must be sane finite fractions, not NaN/inf/negative placeholders that would silently pass a looser "key exists" check.""" | |
| meta = json.loads(_FEATURE_METADATA_PATH.read_text()) | |
| assert "validation_metrics" in meta, ( | |
| "feature_metadata.json missing 'validation_metrics' key " | |
| "(should be present, possibly null) — retrain with " | |
| "`python -m src.models.train_final` to populate it." | |
| ) | |
| vm = meta["validation_metrics"] | |
| if vm is None: | |
| pytest.skip("validation_metrics is null (notebooks/03 not re-run since last build)") | |
| primary = vm["primary"] | |
| for key in ("mape", "smape", "median_ape"): | |
| val = primary[key] | |
| assert isinstance(val, (int, float)) and math.isfinite(val) and val >= 0.0, ( | |
| f"validation_metrics.primary.{key} = {val!r} is not a sane fraction" | |
| ) | |
| rho = primary["spearman_rho"] | |
| assert isinstance(rho, (int, float)) and math.isfinite(rho) and -1.0 <= rho <= 1.0, ( | |
| f"validation_metrics.primary.spearman_rho = {rho!r} out of [-1, 1]" | |
| ) | |
| assert isinstance(primary["n_folds"], int) and primary["n_folds"] > 0, ( | |
| f"validation_metrics.primary.n_folds = {primary.get('n_folds')!r} " | |
| "should be a positive int" | |
| ) | |
| def test_trained_gpu_row_counts_present_and_consistent_with_trained_gpu_ids(self) -> None: | |
| """training_data_tier() needs a per-GPU row count to distinguish below-the-floor from ample data — a gap the older 'trained_gpu_ids' boolean set couldn't express; the two fields must describe the same set, so every id in trained_gpu_ids has a positive count here and vice versa.""" | |
| meta = json.loads(_FEATURE_METADATA_PATH.read_text()) | |
| assert "trained_gpu_row_counts" in meta, ( | |
| "feature_metadata.json missing 'trained_gpu_row_counts' — " | |
| "retrain with `python -m src.models.train_final` to populate it." | |
| ) | |
| counts = meta["trained_gpu_row_counts"] | |
| trained_ids = set(meta["trained_gpu_ids"]) | |
| assert set(counts.keys()) == trained_ids, ( | |
| f"trained_gpu_row_counts keys {sorted(counts.keys())} != " | |
| f"trained_gpu_ids {sorted(trained_ids)}" | |
| ) | |
| for gpu_id, n in counts.items(): | |
| assert isinstance(n, int) and n > 0, ( | |
| f"trained_gpu_row_counts[{gpu_id!r}] = {n!r} should be a positive int" | |
| ) | |