|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import json |
| 4 | +from pathlib import Path |
| 5 | +from typing import Any |
| 6 | + |
| 7 | + |
| 8 | +def recommend_profile( |
| 9 | + discovery: dict[str, Any], |
| 10 | + workload: str | None = None, |
| 11 | + workload_dir: str | None = None, |
| 12 | +) -> dict[str, Any]: |
| 13 | + normalized = _normalize_workload(workload) |
| 14 | + preset = _load_workload_preset(normalized, workload_dir) if normalized else None |
| 15 | + |
| 16 | + vendor = str(discovery.get("vendor", "")) |
| 17 | + policies = discovery.get("policies", []) |
| 18 | + has_thermal = bool(discovery.get("thermal", {}).get("thermal_zones")) |
| 19 | + has_epp = any(policy.get("energy_performance_preference") for policy in policies) |
| 20 | + current_epp = [policy.get("energy_performance_preference") for policy in policies if policy.get("energy_performance_preference")] |
| 21 | + turbo_disabled = discovery.get("intel_pstate", {}).get("no_turbo") == "1" or discovery.get("cpufreq_boost") == "0" |
| 22 | + deepest_latency = max((state.get("latency") or 0) for state in discovery.get("cpuidle_states", [])) if discovery.get("cpuidle_states") else None |
| 23 | + package_temp = _package_temp(discovery) |
| 24 | + laptop_like = _is_laptop_like(discovery) |
| 25 | + |
| 26 | + reasons: list[str] = [] |
| 27 | + warnings: list[str] = [] |
| 28 | + unsupported: list[str] = [] |
| 29 | + confidence = "high" |
| 30 | + |
| 31 | + if not has_thermal: |
| 32 | + confidence = _lower_confidence(confidence) |
| 33 | + warnings.append("Thermal zones are missing; recommendation confidence is reduced.") |
| 34 | + |
| 35 | + profile = "balanced" |
| 36 | + if "AuthenticAMD" in vendor: |
| 37 | + profile = _recommend_for_amd(normalized) |
| 38 | + reasons.append("AMD platform detected; using generic cpufreq and amd-pstate-safe guidance.") |
| 39 | + unsupported.append("Intel-specific HWP/EPP recommendations are suppressed on AMD.") |
| 40 | + if not discovery.get("amd_pstate", {}).get("exists"): |
| 41 | + warnings.append("amd-pstate is not exposed; recommendations stay conservative.") |
| 42 | + elif "arm" in str(discovery.get("arch", "")).lower() or "ARM" in vendor or "aarch64" in str(discovery.get("arch", "")): |
| 43 | + profile = _recommend_for_arm(normalized) |
| 44 | + reasons.append("ARM-style platform detected; using cpufreq/SCMI-style guidance.") |
| 45 | + unsupported.append("Intel-specific EPP and HWP recommendations are suppressed on ARM.") |
| 46 | + else: |
| 47 | + profile = _recommend_for_intel(normalized, has_epp) |
| 48 | + if has_epp: |
| 49 | + reasons.append("Intel platform with EPP exposure allows profile-aligned policy recommendations.") |
| 50 | + else: |
| 51 | + warnings.append("EPP is not exposed; recommendations rely more on governors and boost state.") |
| 52 | + |
| 53 | + if normalized in {"llama-inference", "ai-inference"}: |
| 54 | + profile = "ai-inference" |
| 55 | + reasons.append("AI inference workload maps directly to the ai-inference profile.") |
| 56 | + elif normalized == "latency" and "GenuineIntel" in vendor and has_epp: |
| 57 | + profile = "latency" |
| 58 | + reasons.append("Intel platform with EPP available and latency workload favors the latency profile.") |
| 59 | + elif normalized and preset is not None: |
| 60 | + preset_profile = preset.get("recommended_profile") |
| 61 | + if preset_profile in {"performance", "balanced", "latency", "quiet", "ai-inference"}: |
| 62 | + profile = preset_profile |
| 63 | + reasons.append(f"Workload preset '{preset.get('name')}' recommends the {preset_profile} profile.") |
| 64 | + |
| 65 | + if laptop_like: |
| 66 | + warnings.append("Laptop-like thermal and fan characteristics detected; quiet or balanced may be safer for sustained use.") |
| 67 | + if profile in {"performance", "latency", "ai-inference"}: |
| 68 | + warnings.append("Aggressive profiles on laptop-like systems may reduce thermal headroom.") |
| 69 | + |
| 70 | + if turbo_disabled and profile in {"performance", "latency", "ai-inference"}: |
| 71 | + warnings.append("Turbo or boost is currently disabled; recommended profile may underperform until it is re-enabled.") |
| 72 | + |
| 73 | + if deepest_latency is not None and deepest_latency >= 100 and profile == "latency": |
| 74 | + reasons.append("Deepest C-state latency is high enough that optional idle tuning may help latency-sensitive workloads.") |
| 75 | + |
| 76 | + if package_temp is not None and package_temp >= 80000: |
| 77 | + confidence = _lower_confidence(confidence) |
| 78 | + warnings.append("Thermal headroom appears limited; sustained high-performance profiles may not hold.") |
| 79 | + elif package_temp is not None: |
| 80 | + reasons.append(f"Observed package temperature is about {package_temp / 1000:.0f} C, suggesting current thermal headroom is acceptable.") |
| 81 | + |
| 82 | + optimal_epp = _optimal_epp_for_profile(profile) |
| 83 | + if has_epp and current_epp and all(value == optimal_epp for value in current_epp if optimal_epp is not None): |
| 84 | + reasons.append("Current EPP already matches the recommended profile intent; no changes may be needed.") |
| 85 | + |
| 86 | + if normalized is None: |
| 87 | + reasons.append("No workload preset was specified; recommendation is based on current platform capabilities and safety posture.") |
| 88 | + |
| 89 | + return { |
| 90 | + "recommended_profile": profile, |
| 91 | + "confidence": confidence, |
| 92 | + "reasons": reasons, |
| 93 | + "warnings": warnings, |
| 94 | + "suggested_dry_run_command": f"python cpuoptctl/cpuoptctl.py profile {profile} --dry-run --diff", |
| 95 | + "suggested_restore_command": "python cpuoptctl/cpuoptctl.py restore", |
| 96 | + "unsupported_features": unsupported, |
| 97 | + "workload": normalized, |
| 98 | + } |
| 99 | + |
| 100 | + |
| 101 | +def format_recommendation(report: dict[str, Any]) -> str: |
| 102 | + lines = ["CPUOpt Recommendation", "---------------------"] |
| 103 | + lines.append(f"Recommended profile: {report.get('recommended_profile')}") |
| 104 | + lines.append(f"Confidence: {report.get('confidence')}") |
| 105 | + if report.get("workload"): |
| 106 | + lines.append(f"Workload: {report.get('workload')}") |
| 107 | + lines.append("") |
| 108 | + lines.append("Reasons:") |
| 109 | + for reason in report.get("reasons", []): |
| 110 | + lines.append(f"- {reason}") |
| 111 | + lines.append("") |
| 112 | + lines.append("Warnings:") |
| 113 | + for warning in report.get("warnings", []): |
| 114 | + lines.append(f"- {warning}") |
| 115 | + if not report.get("warnings"): |
| 116 | + lines.append("- none") |
| 117 | + lines.append("") |
| 118 | + lines.append(f"Suggested dry-run command: {report.get('suggested_dry_run_command')}") |
| 119 | + lines.append(f"Suggested restore command: {report.get('suggested_restore_command')}") |
| 120 | + lines.append("") |
| 121 | + lines.append("Unsupported features:") |
| 122 | + for item in report.get("unsupported_features", []): |
| 123 | + lines.append(f"- {item}") |
| 124 | + if not report.get("unsupported_features"): |
| 125 | + lines.append("- none") |
| 126 | + return "\n".join(lines) |
| 127 | + |
| 128 | + |
| 129 | +def _normalize_workload(workload: str | None) -> str | None: |
| 130 | + if not workload: |
| 131 | + return None |
| 132 | + value = workload.strip().lower() |
| 133 | + aliases = { |
| 134 | + "low-latency": "latency", |
| 135 | + "low-latency-trading": "low-latency-trading", |
| 136 | + "llama-inference": "llama-inference", |
| 137 | + "ai-inference": "ai-inference", |
| 138 | + "kernel-build": "kernel-build", |
| 139 | + "low-latency": "latency", |
| 140 | + } |
| 141 | + return aliases.get(value, value) |
| 142 | + |
| 143 | + |
| 144 | +def _load_workload_preset(workload: str, workload_dir: str | None) -> dict[str, Any] | None: |
| 145 | + if workload_dir is None: |
| 146 | + workload_dir = str(Path(__file__).resolve().parents[1] / "examples" / "workloads") |
| 147 | + candidate = Path(workload_dir) / f"{workload}.json" |
| 148 | + if not candidate.exists(): |
| 149 | + return None |
| 150 | + try: |
| 151 | + return json.loads(candidate.read_text(encoding="utf-8")) |
| 152 | + except (OSError, json.JSONDecodeError): |
| 153 | + return None |
| 154 | + |
| 155 | + |
| 156 | +def _recommend_for_intel(workload: str | None, has_epp: bool) -> str: |
| 157 | + if workload == "kernel-build": |
| 158 | + return "performance" |
| 159 | + if workload in {"latency", "low-latency-trading"}: |
| 160 | + return "latency" if has_epp else "performance" |
| 161 | + if workload in {"llama-inference", "ai-inference"}: |
| 162 | + return "ai-inference" |
| 163 | + if workload == "laptop-quiet": |
| 164 | + return "quiet" |
| 165 | + return "balanced" |
| 166 | + |
| 167 | + |
| 168 | +def _recommend_for_amd(workload: str | None) -> str: |
| 169 | + if workload in {"llama-inference", "ai-inference", "kernel-build"}: |
| 170 | + return "balanced" |
| 171 | + if workload in {"latency", "low-latency-trading"}: |
| 172 | + return "latency" |
| 173 | + if workload == "laptop-quiet": |
| 174 | + return "quiet" |
| 175 | + return "balanced" |
| 176 | + |
| 177 | + |
| 178 | +def _recommend_for_arm(workload: str | None) -> str: |
| 179 | + if workload in {"llama-inference", "ai-inference"}: |
| 180 | + return "balanced" |
| 181 | + if workload in {"latency", "low-latency-trading"}: |
| 182 | + return "latency" |
| 183 | + if workload == "laptop-quiet": |
| 184 | + return "quiet" |
| 185 | + return "balanced" |
| 186 | + |
| 187 | + |
| 188 | +def _lower_confidence(confidence: str) -> str: |
| 189 | + order = ["low", "medium", "high"] |
| 190 | + index = order.index(confidence) |
| 191 | + return order[max(0, index - 1)] |
| 192 | + |
| 193 | + |
| 194 | +def _package_temp(discovery: dict[str, Any]) -> int | None: |
| 195 | + zones = discovery.get("thermal", {}).get("thermal_zones", []) |
| 196 | + if not zones: |
| 197 | + return None |
| 198 | + hottest = max(zones, key=lambda zone: zone.get("temp") or -1) |
| 199 | + return hottest.get("temp") |
| 200 | + |
| 201 | + |
| 202 | +def _is_laptop_like(discovery: dict[str, Any]) -> bool: |
| 203 | + model = str(discovery.get("model_name") or "").lower() |
| 204 | + fan_present = any( |
| 205 | + any(name.startswith("fan") for name in device.get("sensors", {})) |
| 206 | + for device in discovery.get("hwmon", []) |
| 207 | + ) |
| 208 | + return fan_present and any(token in model for token in ("core(tm)", "ultra", "mobile", "laptop")) |
| 209 | + |
| 210 | + |
| 211 | +def _optimal_epp_for_profile(profile: str) -> str | None: |
| 212 | + mapping = { |
| 213 | + "performance": "performance", |
| 214 | + "balanced": "balance_performance", |
| 215 | + "latency": "performance", |
| 216 | + "quiet": "power", |
| 217 | + "ai-inference": "performance", |
| 218 | + } |
| 219 | + return mapping.get(profile) |
0 commit comments