Skip to content

J616s: drive the fans from the die temperatures (thermal cooling devices) - #560

Merged
RyanTheTide merged 4 commits into
integration/aurorafrom
j616s/fan-cooling-20261029
Oct 29, 2026
Merged

RyanTheTide merged 4 commits into
integration/aurorafrom
j616s/fan-cooling-20261029

Conversation

@RyanTheTide

Copy link
Copy Markdown
Contributor

On the M4 Pro MacBook Pro 16 (J616s) the fans never respond to load under Linux. The SMC keeps both fans in its own mode 3, and with no operating system asking it for cooling it leaves them off, or switches them on late and ramps them slowly, while the dies heat up. On this machine four busy CPU cores took the CPU die from 52 C to 95 C in under two minutes with both fans at 0 rpm.

On macOS the OS thermal policy provides that demand. Public reports on M4 describe mode 3 as the state the firmware reports while the OS daemon drives cooling, and the Ftst key that releases the fans to the OS. Linux had nothing in that role. #512 made manual fan control work on this firmware but left it opt-in and unused by default.

This series makes the kernel drive the fans as thermal cooling devices, bound to the die temperature zones through the device tree.

  • thermal: power allocator: run fans on active trip points. The power allocator, our default governor, only budgets power actors on passive trips, so a fan mapped to an active trip was never set. It now runs non-power-actor cooling devices on active trips the way bang-bang does (upper state at the trip, nothing below trip minus hysteresis). Power actors and zones without active trips are unchanged.
  • dt-bindings + hwmon: macsmc. With #cooling-cells = <3> on the SMC hwmon node, each fan is a cooling device addressed as <&smc_hwmon fan-index lower upper> (the indexed cooling-spec format the tree already supports). State 0 leaves the fan to the SMC, states 1-8 run it from its minimum to its maximum through the existing manual mode and Ftst unlock. A fan runs at the higher of the cooling state and any fan_control target, so userspace can't take it below what the zones ask for. Unlocking takes the SMC about 9 s, so states are applied from a work item. Any failure leaves the fan with the SMC. Suspend, shutdown and unbind hand every fan back, and resume applies the states again. No new module parameters. Without #cooling-cells nothing changes.
  • arm64: dts: apple: t6040-j616s. The CPU die zone and the seven GPU zones get a ladder of active trips: 75 C min speed, 80/85/90 C step up, 95 C full speed, off once every zone is 10 C below the first trip.

Evidence (J616s, fleet4 tree + this series)

The J616s DT comes from the Stage 2 boot loader, which these tests did not touch. So the DT nodes of the last commit were added to the live tree by a test-only module (in the evidence directory, never for merging), and then macsmc-hwmon was rebound. Both fans then bind to all 8 zones (10 instances per zone).

Run (stress-ng 4 cores, 95 C hard stop) Fans CPU die
fleet4, SMC only 0 rpm throughout 52 to 95.9 C in 117 s, stopped by the guard
this series (first trip 70 C) 1982-2615 rpm (state 2-3), briefly 3880 15 min full run, mean 82.5 C, max 90.5 C, never reached 95 C

After the load the fans went back to SMC control (target 0) within seconds.

  • pm_test (devices) suspend, fans running at state 2: suspend handed the fans back to the SMC. Resume unlocked again and the fans were back at 1982 rpm within 8 s.
  • Unbind, fans at state 2: the fans were handed back, rebind is clean, no warnings.
  • Reboot, fans at state 2: the next boot (fleet4) found the fans under SMC control (target 0).
  • KUnit: macsmc-hwmon-fan has 11 cases, 5 of them new (state to speed mapping, cooling without fan_control, cooling plus a userspace target, the stopped state, a failed unlock). macsmc-parser, macsmc-hwmon and thermal-power-allocator also pass: 37 pass, 1 skip, 0 fail (arm64 qemu). checkpatch is clean.
  • Idle: with the first trip at 70 C, short idle bursts (TCMz 70-75 C for a second) started the fans at minimum for a few seconds, about once a minute. The final DT moves the first trip to 75 C for that reason.

Pending: the idle and load check with the final 75 C first trip. That boot hit the J616s USB stage freeze, which also freezes plain fleet4 boots and is being investigated separately. The state ladder above 75 C is unchanged from the measured run.

Other machines

Neo (J700) has no fan nodes and no active trips, so nothing changes there. No fan cooling device registers, the governor change only acts on active-trip non-power-actor instances, and suspend/resume runs an empty fan loop. Other fan Macs only change if their DT adds #cooling-cells.

Shipping

The DT half reaches the J616s through the device tree in its Stage 2 boot loader, so it needs a Stage 2 rebuild with this DTB. The kernel half is inert until then.

Evidence (logs, fixture, scripts) is in the aurora-ctl reports directory j616s-fan-cooling-20261029. The baseline and test logs are 1 Hz TSVs of the die zone, hottest zone, fan input/target and cooling states.

🤖 Generated with Claude Code

The power allocator only budgets power actors on its passive trips. A
cooling device without power callbacks, such as a fan, mapped to an
active trip of a zone the power allocator governs was never set, so a
device tree fan curve did nothing on kernels that make it the default
governor.

Run those cooling devices the way the bang-bang governor does: when the
zone reaches an active trip, each such instance asks for its upper
state, and when the zone falls below the trip minus its hysteresis it
asks for nothing. A cooling device mapped to several trips takes the
deepest state asked for, so a ladder of active trips gives a stepped
fan curve. Instances that have no target yet (a cooling device bound
after the zone passed the trip, or after resume) are set from the zone
temperature on the next update. Power actors are left to the power
allocator as before, and zones without active trips are unchanged.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
With #cooling-cells = <3> the SMC hwmon node provides its fans as
cooling devices, named by their index among the fan-* nodes as in
<&smc_hwmon fan-index lower upper>. State 0 leaves a fan to the SMC;
the other states run it at speeds spread from its minimum to its
maximum.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
On the M4 Pro MacBook Pro (J616s) the SMC keeps both fans in its own
mode 3. With no operating system asking it for cooling, it leaves them
off, or switches them on late and ramps them slowly, while the dies
heat up: four busy CPU cores took the CPU die from 52 C to 95 C in two
minutes with both fans at 0 rpm. Linux has to ask for the cooling.

With #cooling-cells = <3> on the hwmon node, every fan registers a
cooling device, named by its index among the fan-* nodes. State 0
leaves the fan to the SMC; states 1 to 8 run it at speeds spread evenly
from its minimum to its maximum, through the same manual mode and Ftst
unlock as the fan_control target, which they do not need. A fan runs at
the higher of the userspace target and its cooling state, so userspace
cannot take it below what the thermal zones ask for, and it goes back
to the SMC once neither asks for anything.

Unlocking takes the SMC seconds, so states are applied from a work item.
Any failure leaves the fan with the SMC. Suspend, shutdown and removal
stop the work and hand every fan back; resume applies the cooling
states again. Without #cooling-cells nothing changes.

KUnit covers the state to speed mapping, cooling without fan_control,
cooling together with a userspace target, the stopped state and a
failed unlock.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Make both J616s fans cooling devices of the CPU die zone and the seven
GPU zones. Each zone gets a ladder of active trips: 75 C runs the fans
at their minimum, 80 C, 85 C and 90 C step them up, and 95 C runs them
at full speed. A fan takes the highest state any zone asks for and
returns to the SMC once every zone is 10 C below the first trip.

The first trip sits above the 70-75 C the CPU die touches for a moment
during light desktop bursts, so the fans do not start for those.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant