Skip to content

Commit 57d7ff3

Browse files
committed
Add doctor and framework depth modules
1 parent a1cbc51 commit 57d7ff3

19 files changed

Lines changed: 789 additions & 6 deletions

README.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ CPUOpt-Kernel translates vendor CPU performance controls into safe Linux policy
1515
prefers existing kernel interfaces over raw register writes and treats thermal and fan
1616
control as platform concerns, not as a shortcut for unsafe CPU tuning.
1717

18-
Version `v0.1` is intentionally conservative:
18+
Version `v0.2` remains intentionally conservative:
1919

2020
- Intel-first, user-space-first implementation
2121
- No raw MSR writes
2222
- No thermal protection bypass
2323
- No voltage or overclock manipulation
2424
- Reversible sysfs writes with snapshot/restore
2525
- Dry-run support for every profile application
26+
- `doctor`, `explain`, `intel-hwp`, and compare/telemetry-oriented depth commands
27+
- Read-only Intel MSR telemetry decoder with no write path
2628

2729
## Why this exists
2830

@@ -42,6 +44,11 @@ The first release focuses on safe Intel profile application through sysfs:
4244
- `discover` and `status` report CPUFreq, `intel_pstate`, EPP, EPB, turbo, cpuidle,
4345
thermal, cooling, hwmon, SMT, topology, and NUMA metadata when present
4446
- `profile` maps high-level policies to existing sysfs knobs
47+
- `doctor` highlights problems, opportunities, and safety constraints before tuning
48+
- `explain` documents profile intent, tradeoffs, and safety limits
49+
- `intel-hwp` reports Intel HWP/EPP exposure and optional read-only telemetry availability
50+
- `msr-read --intel --safe` decodes a small read-only allowlist of Intel MSRs
51+
- `compare` provides a safe benchmark/comparison scaffold without automatic package install
4552
- `monitor` provides lightweight live telemetry
4653
- `restore` replays the last reversible snapshot
4754

@@ -65,10 +72,16 @@ cpuopt-kernel/
6572
python3 cpuoptctl/cpuoptctl.py status
6673
python3 cpuoptctl/cpuoptctl.py discover --json
6774
sudo python3 cpuoptctl/cpuoptctl.py profile performance --dry-run
75+
sudo python3 cpuoptctl/cpuoptctl.py profile performance --dry-run --diff
6876
sudo python3 cpuoptctl/cpuoptctl.py profile balanced
6977
sudo python3 cpuoptctl/cpuoptctl.py profile latency --allow-idle-tuning
7078
sudo python3 cpuoptctl/cpuoptctl.py profile quiet
7179
sudo python3 cpuoptctl/cpuoptctl.py profile ai-inference
80+
python3 cpuoptctl/cpuoptctl.py doctor
81+
python3 cpuoptctl/cpuoptctl.py explain performance
82+
python3 cpuoptctl/cpuoptctl.py intel-hwp
83+
sudo python3 cpuoptctl/cpuoptctl.py msr-read --intel --safe
84+
python3 cpuoptctl/cpuoptctl.py compare balanced performance --benchmark stress-ng --duration 30
7285
sudo python3 cpuoptctl/cpuoptctl.py monitor --interval 2
7386
sudo python3 cpuoptctl/cpuoptctl.py restore
7487
python3 cpuoptctl/cpuoptctl.py export-json
@@ -86,14 +99,31 @@ python3 -m unittest
8699

87100
- CPUOpt prefers existing kernel interfaces over raw register writes.
88101
- Every write validates path existence, writability intent, and candidate value selection.
102+
- Dry-run performs zero filesystem modifications.
89103
- Unknown sysfs paths are skipped, not guessed.
90104
- Failed writes are logged and are non-fatal.
91105
- Modified values are snapshotted to `last_state.json` before application.
92-
- v0.1 never writes `/dev/cpu/*/msr`, firmware registers, BIOS settings, or BMC controls.
106+
- `msr-read` is read-only telemetry only and never writes `/dev/cpu/*/msr`.
107+
- CPUOpt never writes firmware registers, BIOS settings, or BMC controls.
93108
- Fan writes are intentionally unimplemented in v0.1 even if fan inspection is enabled.
94109

95110
See [docs/SAFETY.md](C:\Users\ManishKL\Documents\Playground\cpuopt-kernel\docs\SAFETY.md) for the full model.
96111

112+
## Demo
113+
114+
```bash
115+
cpuoptctl doctor
116+
cpuoptctl profile performance --dry-run --diff
117+
cpuoptctl monitor
118+
```
119+
120+
Demo assets:
121+
122+
- [assets/cpuopt-status-demo.txt](C:\Users\ManishKL\Documents\Playground\cpuopt-kernel\assets\cpuopt-status-demo.txt)
123+
- [assets/cpuopt-doctor-demo.txt](C:\Users\ManishKL\Documents\Playground\cpuopt-kernel\assets\cpuopt-doctor-demo.txt)
124+
- [assets/cpuopt-dry-run-demo.txt](C:\Users\ManishKL\Documents\Playground\cpuopt-kernel\assets\cpuopt-dry-run-demo.txt)
125+
- [assets/cpuopt-monitor-demo.txt](C:\Users\ManishKL\Documents\Playground\cpuopt-kernel\assets\cpuopt-monitor-demo.txt)
126+
97127
## Example status output
98128

99129
```text
@@ -120,9 +150,10 @@ Warnings:
120150
- Optional kernel module under `/sys/kernel/cpuopt/`
121151
- Read-only MSR decoding with model-aware guardrails
122152
- Model-specific write allowlists only after documentation and fallback validation
153+
- Workload presets, benchmark compare, and richer doctor/explain flows
123154

124155
## Project metadata
125156

126157
- License: `GPL-2.0-only`
127-
- Current target release: `v0.1.0`
158+
- Current target release: `v0.2.0`
128159
- CI: GitHub Actions runs Python compile checks and `unittest`

assets/cpuopt-doctor-demo.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CPUOpt Doctor
2+
-------------
3+
[OK] intel_pstate active
4+
[OK] EPP exposed
5+
[WARN] turbo disabled
6+
[OK] performance governor available
7+
[INFO] 1 thermal zones detected
8+
[INFO] hwmon fan sensors detected, write disabled by policy
9+
[WARN] deepest C-state has high exit latency; latency profile may benefit from --allow-idle-tuning
10+
[INFO] no restore snapshot available
11+
[WARN] not running as root; write operations may fail
12+
[INFO] MSR device not available for read-only telemetry
13+
[INFO] no uncore frequency interface detected

assets/cpuopt-dry-run-demo.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Planned CPUOpt changes
2+
----------------------
3+
/sys/devices/system/cpu/cpufreq/policy0/scaling_governor
4+
current: powersave
5+
new: performance
6+
7+
/sys/devices/system/cpu/cpufreq/policy0/energy_performance_preference
8+
current: balance_performance
9+
new: performance
10+
11+
/sys/devices/system/cpu/intel_pstate/no_turbo
12+
current: 1
13+
new: 0

assets/cpuopt-monitor-demo.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
timestamp=2026-06-28T13:00:00+00:00
2+
avg_current_freq=1800000
3+
min_current_freq=1800000
4+
max_current_freq=1800000
5+
package_temp=51000
6+
hottest_zone=x86_pkg_temp
7+
turbo_status=1
8+
boost_status=1
9+
policy=policy0 governor=powersave epp=balance_performance current_freq=1800000

assets/cpuopt-status-demo.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## CPUOpt Status
2+
3+
Vendor: GenuineIntel
4+
Model: Intel(R) Core(TM) i7-1280P
5+
Kernel: 6.8.0-test
6+
Scaling driver: intel_pstate
7+
HWP/EPP exposed: yes
8+
Turbo control: intel_pstate/no_turbo
9+
Policies:
10+
policy0 CPUs=0-7 governor=powersave min=400000 max=4700000 epp=balance_performance
11+
Thermals:
12+
x86_pkg_temp temp=51000
13+
Warnings:
14+
Fan control is platform-specific; not directly controlled unless safely exposed.

cpuoptctl/cpuopt_compare.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from __future__ import annotations
2+
3+
import shutil
4+
import subprocess
5+
from typing import Any
6+
7+
try:
8+
from .cpuopt_telemetry import collect_sample
9+
except ImportError:
10+
from cpuopt_telemetry import collect_sample
11+
12+
13+
BENCHMARKS: dict[str, list[str]] = {
14+
"stress-ng": ["stress-ng", "--cpu", "1", "--timeout", "10", "--metrics-brief"],
15+
"sysbench": ["sysbench", "cpu", "--time=10", "run"],
16+
}
17+
18+
19+
def compare_profiles(
20+
profile_a: str,
21+
profile_b: str,
22+
benchmark: str,
23+
duration: int,
24+
sysfs_root: str = "/sys",
25+
) -> dict[str, Any]:
26+
if benchmark not in BENCHMARKS:
27+
return {"ok": False, "reason": f"unsupported benchmark: {benchmark}"}
28+
binary = shutil.which(BENCHMARKS[benchmark][0])
29+
if binary is None:
30+
return {"ok": False, "reason": f"benchmark not installed: {benchmark}"}
31+
32+
before = collect_sample(sysfs_root=sysfs_root)
33+
benchmark_result = _run_benchmark(benchmark, duration)
34+
after = collect_sample(sysfs_root=sysfs_root)
35+
return {
36+
"ok": True,
37+
"profile_a": profile_a,
38+
"profile_b": profile_b,
39+
"benchmark": benchmark,
40+
"duration": duration,
41+
"samples": {
42+
profile_a: before,
43+
profile_b: after,
44+
},
45+
"benchmark_result": benchmark_result,
46+
}
47+
48+
49+
def format_compare_report(result: dict[str, Any]) -> str:
50+
if not result.get("ok"):
51+
return f"Profile comparison unavailable: {result.get('reason')}"
52+
profile_a = result["profile_a"]
53+
profile_b = result["profile_b"]
54+
sample_a = result["samples"][profile_a]
55+
sample_b = result["samples"][profile_b]
56+
lines = ["Profile comparison", "------------------", f"Workload: {result['benchmark']}", f"Duration: {result['duration']}s", ""]
57+
lines.append(f"{'Metric':<24}{profile_a:<14}{profile_b:<14}")
58+
lines.append(f"{'Avg frequency':<24}{_fmt_freq(sample_a.get('avg_current_freq')):<14}{_fmt_freq(sample_b.get('avg_current_freq')):<14}")
59+
lines.append(f"{'Max temperature':<24}{_fmt_temp(sample_a.get('package_temp')):<14}{_fmt_temp(sample_b.get('package_temp')):<14}")
60+
lines.append(f"{'Thermal throttling':<24}{'unknown':<14}{'unknown':<14}")
61+
lines.append(f"{'Elapsed time':<24}{str(result['duration']) + 's':<14}{str(result['duration']) + 's':<14}")
62+
return "\n".join(lines)
63+
64+
65+
def _run_benchmark(benchmark: str, duration: int) -> dict[str, Any]:
66+
cmd = list(BENCHMARKS[benchmark])
67+
if benchmark == "stress-ng":
68+
cmd = ["stress-ng", "--cpu", "1", "--timeout", str(duration), "--metrics-brief"]
69+
if benchmark == "sysbench":
70+
cmd = ["sysbench", "cpu", f"--time={duration}", "run"]
71+
completed = subprocess.run(cmd, capture_output=True, text=True, check=False)
72+
return {"returncode": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
73+
74+
75+
def _fmt_freq(value: Any) -> str:
76+
if not isinstance(value, int):
77+
return "n/a"
78+
return f"{value / 1_000_000:.1f} GHz"
79+
80+
81+
def _fmt_temp(value: Any) -> str:
82+
if not isinstance(value, int):
83+
return "n/a"
84+
return f"{value / 1000:.0f} C"

cpuoptctl/cpuopt_doctor.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from pathlib import Path
5+
from typing import Any
6+
7+
8+
def build_doctor_report(
9+
discovery: dict[str, Any],
10+
state_dir: str,
11+
dev_root: str = "/dev",
12+
is_root: bool | None = None,
13+
) -> list[tuple[str, str]]:
14+
findings: list[tuple[str, str]] = []
15+
policies = discovery.get("policies", [])
16+
intel_pstate = discovery.get("intel_pstate", {})
17+
if intel_pstate.get("status") == "active":
18+
findings.append(("OK", "intel_pstate active"))
19+
elif any(policy.get("scaling_driver") for policy in policies):
20+
findings.append(("INFO", f"scaling driver detected: {policies[0].get('scaling_driver')}"))
21+
else:
22+
findings.append(("WARN", "no scaling driver detected"))
23+
24+
has_epp = any(policy.get("energy_performance_preference") for policy in policies)
25+
findings.append(("OK" if has_epp else "WARN", "EPP exposed" if has_epp else "EPP not exposed"))
26+
27+
turbo_disabled = intel_pstate.get("no_turbo") == "1" or discovery.get("cpufreq_boost") == "0"
28+
findings.append(("WARN" if turbo_disabled else "OK", "turbo disabled" if turbo_disabled else "turbo enabled"))
29+
30+
has_perf_governor = any("performance" in policy.get("available_governors", []) for policy in policies)
31+
findings.append(
32+
("OK" if has_perf_governor else "WARN", "performance governor available" if has_perf_governor else "performance governor unavailable")
33+
)
34+
35+
zone_count = len(discovery.get("thermal", {}).get("thermal_zones", []))
36+
findings.append(("INFO", f"{zone_count} thermal zones detected"))
37+
38+
hwmon = discovery.get("hwmon", [])
39+
fan_detected = any(
40+
any(name.startswith("fan") for name in device.get("sensors", {}))
41+
for device in hwmon
42+
)
43+
if fan_detected:
44+
findings.append(("INFO", "hwmon fan sensors detected, write disabled by policy"))
45+
else:
46+
findings.append(("INFO", "no hwmon fan sensors detected"))
47+
48+
deepest = _deepest_state(discovery)
49+
if deepest is not None and (deepest.get("latency") or 0) >= 100:
50+
findings.append(
51+
(
52+
"WARN",
53+
"deepest C-state has high exit latency; latency profile may benefit from --allow-idle-tuning",
54+
)
55+
)
56+
elif deepest is not None:
57+
findings.append(("INFO", "cpuidle states detected with modest deepest-state latency"))
58+
else:
59+
findings.append(("INFO", "no cpuidle states detected"))
60+
61+
findings.append(
62+
("INFO", "restore snapshot available" if (Path(state_dir) / "last_state.json").exists() else "no restore snapshot available")
63+
)
64+
65+
root_state = is_root if is_root is not None else (_safe_is_root())
66+
findings.append(("OK" if root_state else "WARN", "running with root privileges" if root_state else "not running as root; write operations may fail"))
67+
68+
msr_path = Path(dev_root) / "cpu" / "0" / "msr"
69+
findings.append(("INFO", "MSR device available for read-only telemetry" if msr_path.exists() else "MSR device not available for read-only telemetry"))
70+
71+
findings.append(("INFO", "no uncore frequency interface detected"))
72+
return findings
73+
74+
75+
def format_doctor_report(findings: list[tuple[str, str]]) -> str:
76+
lines = ["CPUOpt Doctor", "-------------"]
77+
for level, message in findings:
78+
lines.append(f"[{level}] {message}")
79+
return "\n".join(lines)
80+
81+
82+
def _safe_is_root() -> bool:
83+
try:
84+
return os.geteuid() == 0
85+
except AttributeError:
86+
return False
87+
88+
89+
def _deepest_state(discovery: dict[str, Any]) -> dict[str, Any] | None:
90+
states = discovery.get("cpuidle_states", [])
91+
if not states:
92+
return None
93+
return max(states, key=lambda state: state.get("latency") or -1)

0 commit comments

Comments
 (0)