Fan Control Service Plan
Rough draft for review, 2026-08-23.
This plan translates documents/05-mi25-driver-interfaces.md into a concrete design for the fan-control service. It is the deliverable of episode 03 (fan-control-interface-plan).
Goal
A C++ service that runs continuously under systemd and keeps the MI25 cool by writing PWM values through the amdgpu hwmon interface. The service must probe and adapt to the driver, not assume a fixed interface.
Interface classification
From documents/05-mi25-driver-interfaces.md.
Control surface (the service writes these)
- pwm1_enable: write 1 (manual) at startup.
- pwm1: write the computed PWM value, 0-255.
- power_dpm_force_performance_level: write “manual” at startup, so the pp_power_profile_mode write is accepted.
- pp_power_profile_mode: write 5 (COMPUTE) at startup, so the SMU boosts eagerly under compute load and idles low.
- The service never writes “high”; forced-high clocks conflict with the COMPUTE profile and waste idle power.
Monitoring surface (the service reads these)
- temp1_input: edge temperature, millidegrees.
- temp2_input: junction temperature, millidegrees.
- temp3_input: mem temperature, millidegrees.
- pwm1: read-back for sanity only; never the control source.
- gpu_busy_percent: informational reporting.
- freq1_input, freq2_input: clock reporting.
- power1_input: power reporting.
Fingerprinting surface (the service reads these at startup)
- /sys/module/amdgpu/version: driver version when present.
- /proc/modules: taint flags O (out-of-tree) and E (unsigned).
- uname release: kernel version, always available.
Optional diagnostics (never required)
- debugfs amdgpu_pm_info: read when present and readable; degrade silently.
Unused
- fan1_input, fan1_target, fan1_min, fan1_max: the tachometer is garbage on this card; never used.
- pwm1_enable value 2 (AUTO): the MI25 firmware fan control does not work; never used.
- /dev and /run: no introspection value for this service.
Card discovery and enumeration
The service must not assume card1. The service controls every MI25 found.
Enumeration algorithm:
- Iterate /sys/class/drm/card*/device/.
- Read vendor and device files.
- Keep every card with vendor 0x1002 (AMD).
- Verify the driver symlink points to amdgpu.
- Find the hwmon directory with pwm1_enable.
- Verify temp1_input, temp2_input, temp3_input, and pwm1 exist.
- A card is controllable when device is 0x6860 (MI25) and steps 4-6 pass.
- Report every AMD card and its controllable state.
The –check option enumerates and reports, then exits. The service itself controls every controllable card.
The SYSFS_ROOT environment variable overrides /sys for tests.
Startup sequence
- Record kernel release and driver version in the startup log.
- Enumerate AMD cards and identify the controllable MI25 set (retry while nodes appear).
- Verify the required attributes exist on each; log what was found.
- For each controllable card: write pwm1_enable = 1.
- For each controllable card: write power_dpm_force_performance_level = manual.
- For each controllable card: write pp_power_profile_mode = 5 (COMPUTE).
- For each controllable card: write the minimum PWM value to pwm1.
- Enter the control loop over every controllable card.
The hwmon node may appear late at boot. Discovery retries with a short delay until at least one controllable card appears or a timeout elapses. On timeout, log the failure and exit nonzero for systemd to restart.
Control algorithm
Modeled on the reference actor6 service.
Constants:
- PWM_STAGES = {10, 50, 75, 100, 150, 200, 255, 255}.
- T_C_STAGES = {0, 40, 50, 60, 70, 80, 100}.
- DELTA_T_INCREASE = 2 (degrees).
- DELTA_T_DECREASE = -3 (degrees).
- INTERVAL = 1 (second).
- PERIOD_REPORT = 100 (seconds between reports when stable).
Loop:
- Read temp2 (junction) and temp3 (mem).
- Control temperature = max(junction, mem).
- If the control temperature changed by at least the delta, recompute the stage and the interpolated PWM target.
- Ramp toward the target: up by 10 per second, down by 6 per second.
- Write the ramp value to pwm1; remember it as the last written value.
- Report edge, junction, mem, and PWM state every PERIOD_REPORT seconds.
The last written PWM value is the source of truth. The pwm1 read-back is a sanity check only.
Failure policy
Defined by the research spike; see documents/07-failure-behavior.md.
- -EBUSY (reset, suspend, not fully initialized): retry, keep last good PWM.
- -ENODEV (device removed): exit nonzero; systemd restarts and re-runs discovery.
- -EINVAL on pwm1 write: re-assert pwm1_enable=1 and retry.
- Silent no-op (bNoFan): treat as a control failure; check read-back.
- Startup discovery timeout: exit nonzero.
The service never drops fan control silently.
Reporting
One line per report, written to standard output (journald):
edge:NN C junction:NN C mem:NN C pwm_have:NN last:NN next:NN want:NN
Units in degrees Celsius and PWM 0-255. Rate: every PERIOD_REPORT seconds when stable, on every change otherwise.
systemd unit
Unit name: amd-mi25-fanctl.service.
- After=modprobe@amdgpu.service, systemd-udevd.service.
- Type=simple.
- ExecStart: the installed service binary.
- Restart=always, RestartSec=2.
- User=root.
- No ExecStartPre sleep; the service’s own discovery retry replaces it.
Implementation structure
- sources/mi25_fan_control.cpp: main, loop, and reporting.
- sources/gpu_interface.cpp/h: AMD card enumeration, controllability check, hwmon reads and writes.
- sources/fan_curve.cpp/h: stage table, interpolation, ramp.
- scripts/install.sh: installs the binary and the systemd unit.
- A CMake build per the project’s C++ flavor.
Test plan
- Portable: sysfs fixture under a temp dir with SYSFS_ROOT; verifies discovery, curve computation, and ramp logic.
- Tool-gated: binary builds and runs with –check against the fixture.
- Live-state: on beast.lan, verifies discovery finds card1, pwm1_enable accepts 1, and pwm1 writes are accepted; reports PASS/WARN/FAIL.
Research-spike outcomes
The research spike is complete; see documents/07-failure-behavior.md.
- The kernel returns -EBUSY during reset, suspend, or uninitialized state.
- The kernel returns -ENODEV after device removal.
- pwm1 writes return -EINVAL unless manual mode is active.
- The bNoFan path silently no-ops pwm1 writes; it is inactive on this card.
- pp_power_profile_mode requires manual level; COMPUTE (5) is verified to work on beast.lan.