Spaces:
Sleeping
Sleeping
| """Unit tests for src/recommend/recommender.py.""" | |
| from __future__ import annotations | |
| import pytest | |
| from src.models.predictor import GpuPredictor | |
| from src.recommend.recommender import ( | |
| GpuRecommender, | |
| _pareto_frontier, | |
| _RANKING_FIELDS, | |
| VALID_RANKING_OBJECTIVES, | |
| ) | |
| # --- Fixtures --- | |
| def predictor() -> GpuPredictor: | |
| return GpuPredictor() | |
| def recommender(predictor: GpuPredictor) -> GpuRecommender: | |
| return GpuRecommender(predictor) | |
| # --- _pareto_frontier (pure function) --- | |
| class TestParetoFrontier: | |
| def _c(self, throughput, price, watts, cost_efficiency=None) -> dict: | |
| """Build a candidate dict for the Pareto objective vector; cost_efficiency defaults to throughput/price (None if price is None), read by the default "tokens_per_dollar" ranking_objective.""" | |
| if cost_efficiency is None and price: | |
| cost_efficiency = throughput / price | |
| return { | |
| "throughput": throughput, | |
| "price_per_gpu_hr": price, | |
| "watts": watts, | |
| "cost_efficiency": cost_efficiency, | |
| "tokens_per_watt": (throughput / watts) if watts else None, | |
| "cost_per_million_tokens": None, | |
| "gpu_id": "test", | |
| } | |
| def test_all_on_frontier_when_no_domination(self): | |
| # A: best throughput; B: best price; C: best watts — no one dominates | |
| candidates = [ | |
| self._c(1000, 200, 500), # A | |
| self._c(500, 50, 500), # B | |
| self._c(600, 150, 100), # C | |
| ] | |
| frontier, dominated = _pareto_frontier(candidates) | |
| assert len(frontier) == 3 | |
| assert len(dominated) == 0 | |
| def test_dominated_candidate_excluded_from_frontier(self): | |
| # D is strictly worse than A on all objectives → dominated | |
| candidates = [ | |
| self._c(1000, 50, 100), # A — best on all | |
| self._c(500, 200, 500), # D — dominated by A | |
| ] | |
| frontier, dominated = _pareto_frontier(candidates) | |
| assert len(frontier) == 1 | |
| assert frontier[0]["throughput"] == 1000 | |
| assert len(dominated) == 1 | |
| def test_frontier_sorted_by_default_ranking_objective(self): | |
| # Default ranking_objective is tokens_per_dollar (cost_efficiency desc); all three are mutually non-dominated on (throughput, price, watts). | |
| candidates = [ | |
| self._c(800, 8, 200), # cost_efficiency 100 | |
| self._c(900, 3, 300), # cost_efficiency 300 | |
| self._c(700, 3.5, 100), # cost_efficiency 200 | |
| ] | |
| frontier, _ = _pareto_frontier(candidates) | |
| assert len(frontier) == 3 | |
| efficiencies = [c["cost_efficiency"] for c in frontier] | |
| assert efficiencies == sorted(efficiencies, reverse=True) | |
| def test_frontier_sorted_by_explicit_ranking_objective(self): | |
| candidates = [ | |
| self._c(800, 8, 200), | |
| self._c(900, 3, 300), | |
| self._c(700, 3.5, 100), | |
| ] | |
| frontier, _ = _pareto_frontier(candidates, ranking_objective="tokens_per_second") | |
| throughputs = [c["throughput"] for c in frontier] | |
| assert throughputs == sorted(throughputs, reverse=True) | |
| def test_invalid_ranking_objective_raises(self): | |
| with pytest.raises(KeyError): | |
| _pareto_frontier([self._c(500, 100, 200)], ranking_objective="bogus") | |
| def test_empty_input(self): | |
| frontier, dominated = _pareto_frontier([]) | |
| assert frontier == [] | |
| assert dominated == [] | |
| def test_single_candidate_on_frontier(self): | |
| frontier, dominated = _pareto_frontier([self._c(500, 100, 200)]) | |
| assert len(frontier) == 1 | |
| assert len(dominated) == 0 | |
| def test_none_ranking_field_sorts_last(self): | |
| # An unpriced GPU (price=None -> cost_efficiency=None) must sort last under the default ranking_objective and must not crash dominance, which reads price_per_gpu_hr directly, not cost_efficiency; neither candidate dominates the other. | |
| candidates = [ | |
| self._c(500, 50, 300), # priced | |
| self._c(800, None, 100), # unpriced; better throughput & watts | |
| ] | |
| frontier, dominated = _pareto_frontier(candidates) | |
| assert len(frontier) == 2 | |
| assert len(dominated) == 0 | |
| assert frontier[-1]["cost_efficiency"] is None # None sorts last | |
| def test_none_price_dominated_by_priced(self): | |
| # An unpriced GPU (price=None, treated as worst-case for dominance) is dominated by any GPU that also matches or beats it on throughput/watts — the missing price alone already supplies the required strict inequality. | |
| candidates = [ | |
| self._c(1000, 100, 100), # beats unpriced on all objectives | |
| self._c(500, None, 300), # unpriced; worse on throughput & watts too | |
| ] | |
| frontier, dominated = _pareto_frontier(candidates) | |
| assert len(frontier) == 1 | |
| assert frontier[0]["price_per_gpu_hr"] == 100 | |
| assert len(dominated) == 1 | |
| assert dominated[0]["price_per_gpu_hr"] is None | |
| def test_objective_vector_computed_once_per_candidate(self): | |
| # _dominates() used to recompute _obj_vector() on every pairwise comparison (O(n) recomputations per candidate, O(n^2) total) — measured 336 field accesses vs. a 24 minimum for 8 candidates; fixed by precomputing each vector once before the O(n^2) loop. | |
| class _CountingDict(dict): | |
| access_count = 0 | |
| def __getitem__(self, key): | |
| if key in ("throughput", "price_per_gpu_hr", "watts"): | |
| _CountingDict.access_count += 1 | |
| return super().__getitem__(key) | |
| # 8 mutually non-dominated candidates: throughput rises while price and watts also rise, so no candidate dominates another. | |
| candidates = [ | |
| _CountingDict(self._c(1000 + i * 37, 2.0 + i * 0.3, 100 + i * 90)) | |
| for i in range(8) | |
| ] | |
| _CountingDict.access_count = 0 | |
| frontier, dominated = _pareto_frontier(candidates) | |
| assert len(frontier) == 8, "fixture must produce zero domination to hit the worst case" | |
| assert len(dominated) == 0 | |
| assert _CountingDict.access_count == 8 * 3, ( | |
| f"expected exactly 24 field accesses (8 candidates x 3 objective fields, " | |
| f"each read once), got {_CountingDict.access_count} — _obj_vector is being " | |
| "recomputed per comparison again" | |
| ) | |
| # --- GpuRecommender.recommend --- | |
| class TestGpuRecommender: | |
| def test_workload_echoed(self, recommender): | |
| result = recommender.recommend( | |
| model_name="gptj", | |
| scenario="Server", | |
| accuracy_tier="base", | |
| framework="tensorrt", | |
| ) | |
| wl = result["workload"] | |
| assert wl["model_name"] == "gptj" | |
| assert wl["scenario"] == "Server" | |
| assert wl["accuracy_tier"] == "base" | |
| assert wl["framework"] == "tensorrt" | |
| def test_model_size_in_workload(self, recommender): | |
| result = recommender.recommend(model_name="llama2-70b", accuracy_tier="99") | |
| # 70B × 1 byte (FP8) = 70 GB | |
| assert result["workload"]["model_size_gb"] == pytest.approx(70.0) | |
| def test_vram_filter_splits_correctly(self, recommender): | |
| # Killer-demo result: llama2-70b tier 99.9 needs FP16 on NVIDIA (140 GB) vs. FP8 on AMD (70 GB) plus KV cache + 10% overhead, so once realistically accounted for, no in-scope NVIDIA GPU (not even H200 SXM's 141 GB) can serve it single-GPU while all three AMD GPUs comfortably fit. | |
| result = recommender.recommend(model_name="llama2-70b", accuracy_tier="99.9") | |
| candidate_ids = {r["gpu_id"] for r in result["frontier"] + result["dominated"]} | |
| vram_filtered_ids = { | |
| f["gpu_id"] for f in result["filtered"] | |
| if f.get("memory_fit_verdict") == "does_not_fit" | |
| } | |
| for gid in ("mi355x", "mi325x", "mi300x"): | |
| assert gid in candidate_ids, f"{gid} should fit but is absent from candidates" | |
| for gid in ("h200_sxm", "h100_sxm", "a100_sxm_80gb", "l4", "rtx4090"): | |
| assert gid in vram_filtered_ids, f"{gid} should be memory-filtered but is missing" | |
| # The two sets must be disjoint — no GPU can be both candidate and filtered | |
| assert candidate_ids.isdisjoint(vram_filtered_ids) | |
| def test_405b_fp16_all_filtered(self, recommender): | |
| # 405B x 2 bytes = 810 GB weights alone; largest GPU is MI355X at 288 GB — none fit even before KV cache/overhead are added. | |
| result = recommender.recommend( | |
| model_name="llama3.1-405b", | |
| accuracy_tier="99.9", | |
| ) | |
| assert len(result["frontier"]) == 0 | |
| assert len(result["dominated"]) == 0 | |
| assert len(result["filtered"]) > 0 | |
| for f in result["filtered"]: | |
| assert f["memory_fit_verdict"] == "does_not_fit" | |
| # Frontier empty ⇒ top_recommendation null and infeasibility populated with cause + relaxable hint | |
| assert result["top_recommendation"] is None | |
| infeasibility = result["infeasibility"] | |
| assert infeasibility is not None | |
| assert infeasibility["reasons"] | |
| assert all(r["category"] == "memory_does_not_fit" for r in infeasibility["reasons"]) | |
| assert any("accuracy_tier" in hint for hint in infeasibility["relaxable"]) | |
| def test_infeasibility_null_when_frontier_nonempty(self, recommender): | |
| # Converse: top_recommendation must mirror frontier[0] and infeasibility must stay null whenever at least one GPU survives. | |
| result = recommender.recommend(model_name="llama2-70b", accuracy_tier="base") | |
| assert result["frontier"] | |
| assert result["top_recommendation"] == result["frontier"][0] | |
| assert result["infeasibility"] is None | |
| def test_tight_verdict_included_not_filtered(self, recommender): | |
| # Golden "tight" case: llama2-70b base tier is FP16 on AMD too, so MI300X needs weights=140 GB + kv≈24 GB + 10% overhead ≈ 180.6 GB against its 192 GB -> util≈0.94, inside the tight band; tight is a disclosure flag, not a hard exclusion, so MI300X must still appear as a real candidate. | |
| result = recommender.recommend(model_name="llama2-70b", accuracy_tier="base") | |
| candidates = result["frontier"] + result["dominated"] | |
| mi300x = next((r for r in candidates if r["gpu_id"] == "mi300x"), None) | |
| assert mi300x is not None, "MI300X should be a candidate (tight, not excluded)" | |
| assert mi300x["memory_fit_verdict"] == "tight" | |
| assert mi300x["vram_fits"] is True | |
| assert not any(f["gpu_id"] == "mi300x" for f in result["filtered"]) | |
| def test_candidate_memory_fit_matches_standalone_predict(self, predictor, recommender): | |
| # recommend() passes its own precomputed memory-fit result into predict_batch() instead of recomputing it (perf fix) — this end-to-end guard checks those precomputed values are actually correct, not just self-consistent, by matching a standalone predictor.predict() call computed from scratch. | |
| result = recommender.recommend(model_name="llama2-70b", accuracy_tier="base") | |
| candidates = result["frontier"] + result["dominated"] | |
| mi300x = next((r for r in candidates if r["gpu_id"] == "mi300x"), None) | |
| assert mi300x is not None | |
| standalone = predictor.predict(gpu_id="mi300x", model_name="llama2-70b", accuracy_tier="base") | |
| for key in ("memory_fit_verdict", "kv_cache_gb", "memory_total_gb", "vram_utilization"): | |
| assert mi300x[key] == standalone[key], ( | |
| f"{key}: recommend()={mi300x[key]!r} standalone predict()={standalone[key]!r}" | |
| ) | |
| def test_budget_filter_respected(self, recommender): | |
| # Budget $1.50/hr filters out expensive GPUs (all in-scope GPUs have pricing, so the is-None arm was dead code); uses batch_size=8, not the default 32, because at batch=32 gptj's KV cache pushed RTX4090/L4 past 24 GB, leaving only a100_sxm_80gb "fitting" — and only because of an unrelated now-fixed bug where A100's missing native FP8 silently fell back to FP16 TFLOPS instead of raising, so batch_size=8 shrinks the KV cache enough that this test's "keeps a cheap GPU" case no longer depends on that bug. | |
| result = recommender.recommend( | |
| model_name="gptj", | |
| accuracy_tier="99", | |
| budget_per_gpu_hr=1.50, | |
| batch_size=8, | |
| ) | |
| candidates = result["frontier"] + result["dominated"] | |
| # RTX4090 ($0.39) and L4 ($0.44) are both priced under $1.50 and fit gptj at this batch size; without this guard the loop below fires zero assertions if pricing changes so all GPUs exceed budget. | |
| assert len(candidates) > 0, ( | |
| "Expected at least one GPU within $1.50/hr — RTX4090 ($0.39) and " | |
| "L4 ($0.44) are currently below this threshold" | |
| ) | |
| for r in candidates: | |
| assert r["price_per_gpu_hr"] <= 1.50 | |
| candidate_ids = {r["gpu_id"] for r in candidates} | |
| assert candidate_ids & {"rtx4090", "l4"}, ( | |
| "Expected RTX4090 and/or L4 to be the candidates keeping this test " | |
| f"green, got {candidate_ids} instead" | |
| ) | |
| assert "a100_sxm_80gb" not in candidate_ids, ( | |
| "a100_sxm_80gb does not support fp8 (accuracy_tier='99') and must " | |
| "never appear as a real candidate — see the precision-support pre-filter" | |
| ) | |
| def test_cheap_budget_top_pick_flagged_as_unmeasured(self, recommender): | |
| # a100_sxm_80gb, l4, and rtx4090 have zero training rows and are also the three cheapest GPUs in pricing.yaml, so a tight budget query structurally tends to surface them as the top recommendation — has_training_data must flag that their predictions are pure spec extrapolation. Uses a light batch/context override so RTX4090's 24 GB still fits gptj (the default batch=32/2048-token assumption needs ~25 GB and would exclude it). | |
| result = recommender.recommend( | |
| model_name="gptj", | |
| accuracy_tier="99", | |
| batch_size=1, | |
| input_tokens=128, | |
| output_tokens=64, | |
| budget_per_gpu_hr=1.0, | |
| ) | |
| assert len(result["frontier"]) > 0, ( | |
| "Expected at least one candidate under $1.00/hr — RTX4090 ($0.39) " | |
| "is currently below this threshold" | |
| ) | |
| top = result["frontier"][0] | |
| assert top["gpu_id"] == "rtx4090" | |
| assert top["has_training_data"] is False | |
| assert top["training_data_tier"] == "none" | |
| # Every candidate and filtered entry must carry both fields regardless of which branch built it — a missing key here would fail Pydantic validation silently at the API layer (extra field dropped, not error), not surface as a crash. | |
| for r in result["frontier"] + result["dominated"] + result["filtered"]: | |
| assert "has_training_data" in r | |
| assert "training_data_tier" in r | |
| def test_mi300x_candidate_flagged_below_floor(self, recommender): | |
| # mi300x has real rows (80) but sits under the 100-row-per-GPU floor — has_training_data alone reports this identically to a well-covered GPU like h100_sxm/h200_sxm (178/283 rows). | |
| result = recommender.recommend(model_name="gptj", accuracy_tier="99") | |
| by_gpu = { | |
| r["gpu_id"]: r | |
| for r in result["frontier"] + result["dominated"] + result["filtered"] | |
| } | |
| assert "mi300x" in by_gpu, "mi300x should appear as a gptj candidate" | |
| assert by_gpu["mi300x"]["has_training_data"] is True | |
| assert by_gpu["mi300x"]["training_data_tier"] == "below_floor" | |
| def test_recommend_calls_training_data_tier_once_per_gpu(self, predictor, recommender, monkeypatch): | |
| # recommender.py's two reject-entry loops each originally called training_data_tier() twice per GPU (once directly, once via has_training_data()) — found via a wraps-mock performance review (16 calls for 8 candidates), fixed to compute the tier once per GPU; llama3.1-405b guarantees every candidate takes the VRAM-fail reject path. | |
| original = predictor.training_data_tier | |
| calls: list[str] = [] | |
| def _counting(gpu_id): | |
| calls.append(gpu_id) | |
| return original(gpu_id) | |
| monkeypatch.setattr(predictor, "training_data_tier", _counting) | |
| result = recommender.recommend(model_name="llama3.1-405b", accuracy_tier="99.9") | |
| n_entries = len(result["frontier"]) + len(result["dominated"]) + len(result["filtered"]) | |
| assert n_entries > 0, "expected candidates to check" | |
| assert len(calls) == n_entries, ( | |
| f"training_data_tier() called {len(calls)}x for {n_entries} result " | |
| "entries, expected exactly 1 call per entry" | |
| ) | |
| def _fr047_vector(c: dict) -> tuple[float, float, float]: | |
| """(throughput, price, watts) normalized to higher-is-better, matching the real dominance check in recommender._pareto_frontier.""" | |
| price, watts = c["price_per_gpu_hr"], c["watts"] | |
| return ( | |
| c["throughput"], | |
| -price if price is not None else float("-inf"), | |
| -watts if watts is not None else float("-inf"), | |
| ) | |
| def test_no_dominated_option_strictly_worse_on_all(self, recommender): | |
| result = recommender.recommend(model_name="llama3.1-8b", accuracy_tier="base") | |
| frontier = result["frontier"] | |
| dominated = result["dominated"] | |
| # Guard: without this assertion the loop below never fires when dominated is empty, giving zero assertions and false confidence — llama3.1-8b base tier fits all 8 in-scope GPUs and h200_sxm is beaten on all three objectives by h100_sxm. | |
| assert len(dominated) >= 1, ( | |
| "Expected at least one dominated candidate for llama3.1-8b base tier — " | |
| "if every in-scope GPU is now Pareto-optimal under (throughput, price, " | |
| "watts), verify this is intentional (pricing/spec change) before updating." | |
| ) | |
| for dom in dominated: | |
| dv = self._fr047_vector(dom) | |
| # At least one frontier member must dominate this candidate | |
| is_dominated_by_frontier = any( | |
| all(fo >= do for fo, do in zip(self._fr047_vector(f), dv)) | |
| and any(fo > do for fo, do in zip(self._fr047_vector(f), dv)) | |
| for f in frontier | |
| ) | |
| assert is_dominated_by_frontier, ( | |
| f"{dom['gpu_id']} is in dominated list but is not dominated by any frontier member" | |
| ) | |
| def test_min_throughput_filter(self, recommender): | |
| # High min_throughput should remove low-performing GPUs | |
| result = recommender.recommend( | |
| model_name="llama2-70b", | |
| accuracy_tier="99", | |
| min_throughput_tok_per_sec=10_000_000, # absurdly high | |
| ) | |
| # Nothing should pass | |
| assert len(result["frontier"]) == 0 | |
| assert len(result["dominated"]) == 0 | |
| def test_amd_99_9_vram_headroom_uses_fp8_model_size(self, recommender): | |
| # AMD at 99.9 tier uses FP8 (70 GB), not FP16 (140 GB): vram_headroom for MI300X (192 GB) must be ~60% (70 GB + 10% overhead), not ~27.1% (140 GB FP16, no overhead) — this would catch the recommender computing vram_headroom from the workload-level FP16 size instead of the per-GPU FP8 total; uses a minimal batch/context override so KV cache doesn't obscure the comparison. | |
| 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 should be a candidate for llama2-70b tier 99.9" | |
| expected_headroom = (192 - 70 * 1.10) / 192 # weights + 10% overhead, ~zero KV cache | |
| got_headroom = mi300x["vram_headroom"] | |
| assert got_headroom == pytest.approx(expected_headroom, abs=0.01), ( | |
| f"MI300X vram_headroom={got_headroom:.3f} — expected ~{expected_headroom:.3f}" | |
| " (FP8 70 GB model); got FP16 headroom instead?" | |
| ) | |
| def test_frontier_is_pareto_optimal_gptj(self, recommender): | |
| # gptj fits all 8 in-scope GPUs — full-field Pareto test with no VRAM pre-filter; h100_sxm beats h200_sxm on all three objectives (same watts, lower price, higher throughput) so h200_sxm is dominated, while the other 7 trade off and are mutually Pareto-optimal. Verifies: frontier non-empty, every dominated GPU correctly classified, no frontier member dominates another. | |
| result = recommender.recommend(model_name="gptj", accuracy_tier="99") | |
| frontier = result["frontier"] | |
| dominated = result["dominated"] | |
| assert len(frontier) >= 1, "Expected ≥ 1 Pareto-optimal GPU for gptj" | |
| assert len(dominated) >= 1, ( | |
| "Expected ≥ 1 dominated candidate for gptj — " | |
| "8 in-scope GPUs with diverse specs; not all can be Pareto-optimal" | |
| ) | |
| # Every GPU in dominated must be dominated by at least one frontier member. | |
| for dom in dominated: | |
| dv = self._fr047_vector(dom) | |
| is_dominated_by_frontier = any( | |
| all(fo >= do for fo, do in zip(self._fr047_vector(f), dv)) | |
| and any(fo > do for fo, do in zip(self._fr047_vector(f), dv)) | |
| for f in frontier | |
| ) | |
| assert is_dominated_by_frontier, ( | |
| f"{dom['gpu_id']} is in dominated list but not dominated by any frontier member" | |
| ) | |
| # No frontier member may dominate another. | |
| for i, a in enumerate(frontier): | |
| av = self._fr047_vector(a) | |
| for j, b in enumerate(frontier): | |
| if i == j: | |
| continue | |
| bv = self._fr047_vector(b) | |
| dominates = ( | |
| all(bo >= ao for bo, ao in zip(bv, av)) | |
| and any(bo > ao for bo, ao in zip(bv, av)) | |
| ) | |
| assert not dominates, ( | |
| f"Frontier member {b['gpu_id']} dominates {a['gpu_id']} — " | |
| "gptj frontier is not Pareto-optimal" | |
| ) | |
| # --- recommend() precision-support pre-filter --- | |
| class TestRecommendPrecisionSupport: | |
| """a100_sxm_80gb has no native FP8; before this filter existed, recommend() sent it straight to predict_batch(), which silently substituted fp16's TFLOPS and surfaced a fabricated ~831 tok/s "dominated" entry for gptj instead of raising.""" | |
| def test_unsupported_precision_gpu_is_filtered_not_crashed(self, recommender): | |
| # gptj is small enough that a100 would otherwise pass the VRAM check and reach predict_batch() — this isolates the precision filter from the memory-fit filter (unlike llama2-70b, which also fails on VRAM and would mask this). | |
| result = recommender.recommend(model_name="gptj", accuracy_tier="99") | |
| candidate_ids = {r["gpu_id"] for r in result["frontier"] + result["dominated"]} | |
| assert "a100_sxm_80gb" not in candidate_ids | |
| entry = next(f for f in result["filtered"] if f["gpu_id"] == "a100_sxm_80gb") | |
| assert entry["memory_fit_verdict"] == "does_not_fit" | |
| assert entry["pred_throughput_tok_per_sec"] == 0.0 | |
| assert "fp8" in entry["reject_reason"] | |
| assert "not supported" in entry["reject_reason"] | |
| def test_unsupported_precision_gpu_never_reaches_predict_batch(self, recommender, monkeypatch): | |
| # Assert the mechanism, not just the outcome: predict_batch() must never even be called with a100 in the request list, since it would raise and crash the whole recommend() call for every other candidate GPU too. | |
| original = recommender._predictor.predict_batch | |
| def _spy(requests): | |
| gpu_ids = [r["gpu_id"] for r in requests] | |
| assert "a100_sxm_80gb" not in gpu_ids, ( | |
| "a100_sxm_80gb must be filtered out before predict_batch() is called" | |
| ) | |
| return original(requests) | |
| monkeypatch.setattr(recommender._predictor, "predict_batch", _spy) | |
| recommender.recommend(model_name="gptj", accuracy_tier="99") | |
| def test_supported_precision_gpus_unaffected(self, recommender): | |
| # The filter must not over-exclude: GPUs that DO support fp8 must still reach candidates/filtered-for-other-reasons as before, not get swept up by this new filter. | |
| result = recommender.recommend(model_name="gptj", accuracy_tier="99") | |
| all_ids = { | |
| r["gpu_id"] | |
| for r in result["frontier"] + result["dominated"] + result["filtered"] | |
| } | |
| assert all_ids == set(recommender._in_scope_ids) | |
| precision_reasons = { | |
| f["gpu_id"]: f["reject_reason"] | |
| for f in result["filtered"] | |
| if "not supported" in f["reject_reason"] | |
| } | |
| assert set(precision_reasons) == {"a100_sxm_80gb"} | |
| # --- recommend() serving-shape validation --- | |
| class TestRecommendServingShapeValidation: | |
| """recommend()'s memory-fit pre-filter uses batch_size/input_tokens/output_tokens before any predict_batch() range validation; without an explicit check here, an out-of-range value could silently reach every GPU as "does_not_fit" and return a normal-looking response instead of raising.""" | |
| def test_out_of_range_serving_shape_raises(self, recommender, kwargs): | |
| # match= pins this to the parametrized field actually being rejected — otherwise a future reordering of recommend()'s validation could make this pass for the wrong reason. | |
| param_name = next(iter(kwargs)) | |
| with pytest.raises(ValueError, match=f"Invalid {param_name}"): | |
| recommender.recommend(model_name="gptj", accuracy_tier="99", **kwargs) | |
| def test_raises_before_any_gpu_is_touched(self, recommender): | |
| # A batch_size this large makes every in-scope GPU "does_not_fit" — the exact condition that let bad input slip past predict_batch()'s validation before this test's namesake fix. | |
| with pytest.raises(ValueError, match="Invalid batch_size"): | |
| recommender.recommend(model_name="gptj", batch_size=999_999_999) | |
| # --- recommend() accuracy_tier/scenario/framework validation --- | |
| class TestRecommendCategoricalValidation: | |
| """recommend() must be safe to call directly with untrusted input (same contract as predictor._validate()); before this check, an invalid accuracy_tier raised an uncaught KeyError instead of ValueError (so main.py's handler wouldn't catch it), and invalid scenario/framework could silently pass through unvalidated whenever every candidate GPU was excluded before predict_batch()'s own checks — not reachable via the shipped app's input constraints, but recommend() itself had no independent guarantee.""" | |
| def test_invalid_accuracy_tier_raises_value_error_not_key_error(self, recommender): | |
| with pytest.raises(ValueError, match="Invalid accuracy_tier"): | |
| recommender.recommend(model_name="gptj", accuracy_tier="fp99000; DROP TABLE") | |
| def test_invalid_scenario_raises(self, recommender): | |
| # Uses llama3.1-405b, which fails memory-fit on every GPU so none reach predict_batch(), isolating recommend()'s own scenario check from predict_batch()'s — gptj (the original choice) passes for the wrong reason since it fits most GPUs and predict_batch()'s own _validate() catches the bad scenario anyway, confirmed by mutation-testing: removing recommend()'s own check with gptj still passed. | |
| with pytest.raises(ValueError, match="Invalid scenario"): | |
| recommender.recommend( | |
| model_name="llama3.1-405b", accuracy_tier="99.9", scenario="Interactive", | |
| ) | |
| def test_invalid_framework_raises(self, recommender): | |
| # Same reasoning as test_invalid_scenario_raises above. | |
| with pytest.raises(ValueError, match="Invalid framework"): | |
| recommender.recommend( | |
| model_name="llama3.1-405b", accuracy_tier="99.9", framework="pytorch", | |
| ) | |
| # --- ranking_objective — user-selectable Pareto-set ranking scalar --- | |
| class TestRankingObjective: | |
| """recommend()'s Pareto-optimal (rank-1) set is sorted by a user-selectable scalar — tokens_per_dollar (default), tokens_per_second, tokens_per_watt, or lowest_cost_per_million_tokens.""" | |
| def test_valid_ranking_objectives_contains_all_four_fr048_scalars(self): | |
| assert VALID_RANKING_OBJECTIVES == { | |
| "tokens_per_dollar", "tokens_per_second", | |
| "tokens_per_watt", "lowest_cost_per_million_tokens", | |
| } | |
| def test_invalid_ranking_objective_raises(self, recommender): | |
| with pytest.raises(ValueError, match="Invalid ranking_objective"): | |
| recommender.recommend(model_name="gptj", ranking_objective="bogus") | |
| def test_default_matches_explicit_tokens_per_dollar(self, recommender): | |
| default = recommender.recommend(model_name="gptj", accuracy_tier="99") | |
| explicit = recommender.recommend( | |
| model_name="gptj", accuracy_tier="99", ranking_objective="tokens_per_dollar", | |
| ) | |
| assert [c["gpu_id"] for c in default["frontier"]] == [ | |
| c["gpu_id"] for c in explicit["frontier"] | |
| ] | |
| def test_workload_echoes_ranking_objective(self, recommender): | |
| result = recommender.recommend(model_name="gptj", ranking_objective="tokens_per_watt") | |
| assert result["workload"]["ranking_objective"] == "tokens_per_watt" | |
| def test_frontier_sorted_by_each_objective( | |
| self, recommender, objective, field, higher_is_better, | |
| ): | |
| # Direct wiring check first: catches _RANKING_FIELDS mapping the wrong field to `objective` immediately, regardless of what any dataset's values happen to look like. | |
| assert _RANKING_FIELDS[objective] == (field, higher_is_better) | |
| # Uses llama2-70b, not gptj: gptj/tier-99's frontier order is a false-confidence trap where cost_efficiency, throughput, and tokens_per_watt all happen to agree, so a mutation mapping "tokens_per_watt" to the wrong field still produces a monotonic column by coincidence (confirmed by mutation-testing) — llama2-70b/tier-99 genuinely diverges across all three ratio fields, so sorting by the wrong field produces a visibly non-monotonic column instead. | |
| result = recommender.recommend( | |
| model_name="llama2-70b", accuracy_tier="99", ranking_objective=objective, | |
| ) | |
| values = [c[field] for c in result["frontier"]] | |
| assert len(values) >= 3, "llama2-70b/99 should have >=3 frontier GPUs to make sorting meaningful" | |
| assert values == sorted(values, reverse=higher_is_better) | |
| def test_watts_tokens_per_watt_cost_per_million_hand_verified(self, predictor, recommender): | |
| # Hand-verifies against gpu_specs.yaml's tdp_w, a standalone predictor.predict() call, and recommender._pricing — not mi300x's own fields — so a bug that corrupts throughput/price and tokens_per_watt/cost_per_million_tokens together can't also corrupt the expected value the same way; mutation-testing found the original version (deriving "expected" from mi300x's own fields) still passed even when all three were wired to the same wrong source. | |
| result = recommender.recommend(model_name="gptj", accuracy_tier="99") | |
| candidates = result["frontier"] + result["dominated"] | |
| mi300x = next(c for c in candidates if c["gpu_id"] == "mi300x") | |
| assert mi300x["watts"] == 750 | |
| standalone = predictor.predict(gpu_id="mi300x", model_name="gptj", accuracy_tier="99") | |
| real_tput = standalone["pred_throughput_tok_per_sec"] | |
| real_price = recommender._pricing["mi300x"] | |
| assert mi300x["throughput"] == pytest.approx(real_tput) | |
| assert mi300x["price_per_gpu_hr"] == pytest.approx(real_price) | |
| assert mi300x["tokens_per_watt"] == pytest.approx(real_tput / 750) | |
| expected_cpm = (real_price / 3600) / (real_tput / 1_000_000) | |
| assert mi300x["cost_per_million_tokens"] == pytest.approx(expected_cpm) | |
| def test_filtered_entry_carries_watts_but_not_derived_ratios(self, recommender): | |
| # llama3.1-405b fails memory-fit on every GPU — filtered entries still carry the GPU's static watts (a spec fact) but not tokens_per_watt/cost_per_million_tokens, which need a real throughput prediction to be meaningful. | |
| result = recommender.recommend(model_name="llama3.1-405b", accuracy_tier="99.9") | |
| assert result["frontier"] == [] | |
| assert len(result["filtered"]) > 0 | |
| for f in result["filtered"]: | |
| assert f["watts"] is not None | |
| assert f["tokens_per_watt"] is None | |
| assert f["cost_per_million_tokens"] is None | |