Skip to content

J713: G16 performance governor, and the PMP idle floor it needs - #455

Merged
erykwieliczko merged 2 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/g16-dvfs-governor
Nov 4, 2026
Merged

erykwieliczko merged 2 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/g16-dvfs-governor

Conversation

@erikwb

@erikwb erikwb commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

The G16 firmware runs at the performance state the host publishes in the three Globals selectors, busy or idle. The host published the thermal ceiling, so on the J713 the GPU sat at S9 (1470 MHz, 1000 mV) with nothing to draw:

J713 GPU rail, 60 fps desktop S1-S3 S4 S6 S8 S9
mW 68-90 264 578 861 1233

Idle with nothing redrawing, the rail still drew about 1.09 W at S9.

Scope: J713 / T8132 only

  • The governor is in the g16 backend, which registers only apple,agx-t8132.
  • The PMP driver probes only apple,j713, and its rail reader selects apple,t8132-smc.
  • T6040 (J616s) routes to g17 with its own power manager, so these commits do not run on it.
  • If T6040 moves to the g16 backend later, the S3 floor and the utilization thresholds would need their own G16S measurements. The S3 energy argument below was measured on the J713 only.

The PMP idle floor (patch 1)

apple-pmp-thermal treats a reading of less than 0.1 W across the P-CPU, E-CPU and GPU rails as a firmware reset image. It then:

  • reports -ENODATA;
  • retains the minimum CPU and GPU ceilings;
  • fails the hwmon rails with -EAGAIN.

With the GPU idling properly, an idle J713 reads about 80 mW on those rails (P-CPU 40-55, E-CPU 9, GPU 27). As a result:

  • the ceilings sat at the minimum while idle, with the CPUs at 900 MHz;
  • the GPU budget climbed back one state at a time once work arrived.

A reset image reads zero on both CPU rails, which the running reader can't, so that is now the only reading rejected. This patch has to land with or before the governor.

The governor (patch 2)

The host now requests a state from the GPU's busy share of each 20 ms window. Busy time runs from publication to drain. The thermal and user ceilings still bound the request, including ceilings below S3.

  • Up, on the last window: a window at least 70% busy goes straight to the top state, and 40% or more steps up one.
  • Down, on a utilization averaged over about four windows: under 25% steps down one, and under 10% drops to S3. A gap longer than four windows restarts the average.
  • Floor S3. On a J713 desktop load, S3 draws no more than S1 and runs at 2.4 times its clock, while S4 and up cost several times as much.
  • Idle: an idle engine polls once more per window until it is back at S3, then stops.
  • Recovery: flights dropped without a drain end the busy span. After a fail-stop, poll returns before the governor, so the published state is left as it was (unchanged from before this series).

The policy and its accounting are policy::Governor in power/policy.rs. The call sites are:

  • publication (engine.rs);
  • drain (poll.rs);
  • the power service (power.rs);
  • the idle re-arm (run_poll).

The debugfs stats line gains demand and utilization.

Paired A/B on the J713

Arm B adds only the two commits of this PR to arm A, so these two commits are the only difference.

  • A (baseline): the J713 integration tree used for daily testing, with neither commit.
  • B: A plus the PMP commit, then the governor commit. B's g16/ and apple-pmp-thermal.c match this PR's head byte for byte.

Both arms ran on the same machine:

  • on battery, with the backlight at 250/500;
  • with idle and screen locking inhibited and the visible workspace empty;
  • with the same userspace and GL stack (Mesa 26.3.0-devel 4911479bf9, glmark2 2023.01, Hyprland at 2560x1664);
  • one boot each, with one script and the same parameters.

Sampling:

  • Power and state: every 1 s, from the SMC battery power, the PMP P-CPU/E-CPU/GPU rails, all PMP temperatures and the published and demanded state from asahi-g16/stats.
  • Frame times: an LD_PRELOAD shim timestamps the return of every eglSwapBuffers (CLOCK_MONOTONIC). A missed frame at 60 Hz is an interval over 25 ms.

Protocol:

  1. Settle until the die hotspot is at or below 53 °C, then wait 30 s.
  2. Idle: 5 × 60 s.
  3. Vsync pacing: 3 × 30 s each of build, shadow and desktop:effect=blur, fullscreen with --swap-mode fifo. Each run follows a 30 s rest and a ≤ 53 °C gate, and the first 30 frames are skipped.
  4. Loop: 3 × the default glmark2 suite, fullscreen and unthrottled. Each run follows a 120 s rest and a ≤ 53 °C gate.

Values are the mean ± sd of the per-run means, followed by the raw runs.

Idle desktop, nothing redrawing (5 × 60 s)

A B A runs B runs
Battery (mW) 7319 ± 44 6063 ± 7 7269, 7283, 7319, 7348, 7377 6075, 6060, 6058, 6063, 6059
GPU rail (mW) 1090 ± 2 27 ± 0 1088, 1089, 1089, 1090, 1094 27, 27, 27, 27, 27
P-CPU rail (mW) 133 ± 28 43 ± 1 101, 112, 133, 147, 172 42, 43, 43, 42, 43
E-CPU rail (mW) 12 ± 0 12 ± 0 12, 13, 13, 12, 12 12, 12, 12, 12, 11
Within-run battery sd (mW) 102 ± 54 25 ± 16
Published state S9 S3

Vsync pacing, fullscreen fifo at 60 Hz (3 × 30 s per scene)

Scene Arm Frames Mean ms p50 ms p99 ms Max ms Missed GPU rail mW Battery mW States
build A 1772, 1772, 1772 16.67 ± 0.00 16.66 ± 0.00 17.29 ± 0.02 17.8, 17.5, 17.5 0, 0, 0 1277 ± 2 7696 ± 8 S9 100%
build B 1772, 1773, 1773 16.67 ± 0.00 16.67 ± 0.00 17.17 ± 0.02 17.6, 17.5, 17.5 0, 0, 0 106 ± 1 6368 ± 2 S3 100%
shadow A 1772, 1772, 1772 16.67 ± 0.00 16.67 ± 0.00 17.32 ± 0.06 17.5, 17.7, 17.5 0, 0, 0 1369 ± 4 8111 ± 24 S9 100%
shadow B 1772, 1772, 1773 16.67 ± 0.00 16.67 ± 0.00 17.16 ± 0.02 17.4, 17.4, 17.5 0, 0, 0 1327 ± 5 7950 ± 9 S9 100%
blur A 1771, 1771, 1771 16.67 ± 0.00 16.66 ± 0.00 17.23 ± 0.10 17.5, 17.6, 17.5 0, 0, 0 1864 ± 25 9005 ± 9 S9 100%
blur B 1771, 1771, 1771 16.67 ± 0.00 16.66 ± 0.00 17.12 ± 0.02 17.4, 17.5, 17.5 0, 0, 0 1824 ± 39 8910 ± 45 S9 100%

glmark2 default suite, fullscreen, unthrottled (3 runs per arm, 331 s each)

A B A runs B runs
Score 574 ± 2 573 ± 1 573, 574, 576 572, 574, 574
Start die hotspot (°C) 52.6 ± 0.5 47.8 ± 1.0 52.0, 52.8, 53.0 46.8, 48.0, 48.8
GPU rail mean (mW) 2101 ± 10 2079 ± 10 2092, 2112, 2099 2068, 2079, 2089
Battery mean (mW) 9728 ± 25 9623 ± 17 9707, 9721, 9755 9604, 9632, 9635
Frame p50 (ms) 1.30 ± 0.00 1.30 ± 0.00 1.30, 1.30, 1.30 1.30, 1.30, 1.30
Frame p99 (ms) 5.65 ± 0.01 5.70 ± 0.01 5.66, 5.65, 5.64 5.71, 5.71, 5.69
Peak PMP die hotspot (°C) 64.4 ± 0.4 63.3 ± 2.0 64.0, 64.8, 64.5 61.5, 65.5, 63.0
Peak Tp000m (°C) 64.2 ± 0.4 61.8 ± 0.7 63.8, 64.5, 64.5 61.0, 62.0, 62.2
Peak Tp001m (°C) 64.3 ± 0.4 62.3 ± 0.8 64.0, 64.8, 64.2 61.5, 62.5, 63.0
Peak Te000m (°C) 60.0 ± 0.4 57.6 ± 0.5 59.5, 60.2, 60.2 57.0, 57.8, 58.0
Peak Te001m (°C) 61.9 ± 0.4 61.5 ± 3.5 61.5, 62.2, 62.0 59.2, 65.5, 59.8
Peak Ta000m (°C) 58.2 ± 0.4 55.6 ± 0.5 57.8, 58.2, 58.5 55.0, 55.8, 56.0

The published state during the loops was S9 in 994 of 994 samples for A and 993 of 994 for B.

How to read this

  • Idle: 1256 mW less battery draw (−17%), almost all of it GPU rail.

    • A's P-CPU rail climbs across its idle runs, while B's stays flat at about 43 mW. This is consistent with A running hotter.
  • 60 Hz pacing:

    • Neither arm missed a frame in 9 runs each, and B's p99 is no worse than A's.
    • build stays at S3, with the GPU rail down from 1277 to 106 mW.
    • shadow and blur need S9 in both arms.
  • Unthrottled: the score is unchanged within the run-to-run spread. Two scenes are a little lower with B, and their runs do not overlap with A's:

    • ideas: 633-647 FPS vs 655-661 (−2.6%);
    • build: about −1%.

    These are the burstiest scenes, which suggests brief step-downs inside the 20 ms windows that 1 s sampling does not resolve. All other scenes are within ±1%.

  • Temperatures: A loop run starts after a 120 s rest, once the die is at or below 53 °C. B idles cooler, so B's runs started about 5 °C lower. The lower B peaks are therefore not evidence of a cooler load. They show only that B is not hotter under the same load. Ambient was not measured, and the arms ran about 11 h apart.

Notes

  • Both arms read ENODATA from two PMP temperature channels at similar counts: Ta000m 443 vs 446, Te000m 119 vs 111. This is unaffected by these commits.
  • There is one J713 and one run per arm, with no J616s or other G16 hardware. See Scope.

Testing

  • Host tests (test-thermal-policy.py): 10 governor cases, run against the production policy::Governor:
    • busy time from publication to drain;
    • busy spans carried to each window's end;
    • flights dropped by recovery without a drain ending the span;
    • the idle re-arm returning to S3 and then stopping;
    • thermal and user ceilings, including those below S3.
  • Call-site check: the same script verifies that the governor is connected at publication, drain, power service and idle re-arm.
  • Mutants: seven single-line mutants of the accounting are all caught. They are:
    • span not carried to window end;
    • recovery drop not closed;
    • never decaying;
    • decaying while busy;
    • ceiling ignored;
    • publication restarting the span;
    • drain not counted.
  • Static checks: rustfmt and checkpatch --strict are clean on both patches. The tree builds with no new warnings.
  • J713: the A/B above, plus daily use on a kernel carrying this series.

@RyanTheTide

Copy link
Copy Markdown
Contributor

Hold for the performance evidence and the exact working T6040 source pin. The idle-power improvement reported here is useful, but the PR body needs a reproducible method and paired raw before/after runs with repeat count, duration, sample cadence and variance. The 620-643 offscreen build scores and 577 full score are not a paired run table; there is no frame-time or peak-temperature comparison. Keep this draft until those measurements are in the body.

The chip scope at 338a2ca3ec70 is narrower than the hardware generation name suggests. g16/mod.rs registers only apple,agx-t8132; the new governor has no further chip check, so it affects every device using that backend, currently T8132 only. T6040 selects ProbeConfig::G17 and g17::Gpu, with a separate power manager. Its GpuGen::G16 value is a userspace generation label. I also checked the public J616s branch wip/j616s-gpu-20261026 at c6d9623c10a7, including the eda12929fb16 anchor: it has that same routing. Please identify any different verified working pin before calling this a T6040 governor change. If that pin moves T6040 to the g16 backend, the S3 floor and utilization policy apply without a T8132 guard and need G16S validation.

The PMP rail-validity change has no local chip check, but the entire driver's probe rejects machines other than apple,j713 and its rail reader selects apple,t8132-smc. It is effectively J713/T8132-only at these pins, not a T6040 idle-floor fix.

For J616s, both A/B kernels must contain #453's same accepted firmware identity and the same working GPU bring-up commits. #453 changes exact firmware-identity admission, not the governor or backend routing. Compare the working baseline without either #455 commit against that identical source plus the PMP commit followed by the governor commit. Record idle GPU rail power, repeated glmark2 scores and frame pacing, and peak GPU/SoC temperature during the same fixed glmark2 loop, with identical display, power source, software, ambient/start temperature and thermal policy. Put the raw paired results and variance in this PR. A failed-probe baseline cannot establish a performance improvement. Resolve the routing mismatch first; unchanged g17 results would not exercise this new governor.

The pure policy retains thermal/user ceilings after the S3 floor, including ceilings below S3. The added tests cover utilization, smoothing and demand thresholds; add coverage of production busy/drain accounting, recovery and the idle rearm path so those connections are checked too. The S3 energy argument is measured on J713 and cannot be assumed for G16S.

A separate software-gate run for this head plus #453 is queued through the ordinary buildslot. The earlier J713 train passed both full WERROR builds and 275 KUnit tests, but that source does not contain #455 and is not evidence for this governor. I will update this same comment with the new run results. No J616s measurements were performed in this review.

Merge only after the benchmark and routing gates, the shared-code regression boots required by the train, and the J616s A/B results. Keep #453 in both benchmark arms; within this PR, apply the PMP validity change before the governor. The existing J713 speaker-protection and suspend holds remain separate requirements.

@erikwb

erikwb commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. I've updated the body and pushed a new head.

  • Paired measurements: A is the J713 integration tree without either commit. B is the same tree plus the PMP commit, then the governor commit.
    • Same machine, display, power source (battery), userspace and GL stack.
    • Repeats: idle 5 × 60 s; vsync pacing 3 × 30 s for each of three scenes; the full glmark2 suite 3 × 331 s.
    • 1 s power, state and temperature sampling, plus per-frame swap timestamps.
    • Every run is listed with its mean ± sd.
    • Result: idle draw falls by 17%; no missed frames at 60 Hz in either arm; the glmark2 score is unchanged at 574 ± 2 vs 573 ± 1.
    • The body also states the limits: B's runs started cooler, and two bursty scenes are 1-2.6% lower.
  • Routing: agreed. The change is J713/T8132 only at these pins (g16 registers only apple,agx-t8132, and the PMP driver probes only apple,j713). I've removed any implication beyond that, and I have no T6040 pin that routes to g16. Since T6040 runs g17, this PR doesn't exercise it. A J616s A/B would only become meaningful if that routing changed, and then it would need its own G16S thresholds. I don't have J616s hardware to run it.
  • Coverage: the accounting now lives in policy::Governor, and the host test drives it directly. It covers publication to drain, spans across windows, recovery drops without a drain, the idle re-arm back to S3, and ceilings below S3. A check verifies the four call sites, and seven mutants of the accounting are all caught.
  • Order: the PMP validity commit still comes before the governor.

erikwb and others added 2 commits November 4, 2026 10:38
The power work treats a reading of less than 0.1 W across the P-CPU,
E-CPU and GPU rails as a firmware reset image: it reports -ENODATA,
retains the minimum CPU and GPU ceilings and fails the hwmon rails with
-EAGAIN. An idle J713 reads below that once its GPU idles at a low
performance state (P-CPU 40-55 mW, E-CPU 9 mW, GPU 27 mW): the
ceilings then sat at the minimum while idle, the CPUs ran at 900 MHz,
and the GPU budget climbed back one state at a time once work arrived.

A reset image reads zero on both CPU rails, which the running reader
cannot. Reject only that.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The firmware runs at the state the three Globals selectors publish,
busy or idle, and the host published the thermal ceiling: on the J713
the GPU sat at S9 (1470 MHz, 1000 mV) with nothing to draw. Its rail
drew 1.06 W idle and 1.25 W for a 60 fps desktop, against 68-90 mW
for the same desktop at S1-S3.

Request a state from the GPU's busy share of each 20 ms window, between
S3 and the thermal and user ceiling, which still bound it: the top state
when a window was at least 70% busy, one step up at 40%, and down on a
utilization averaged over about four windows, one step under 25% and to
S3 under 10%. Busy time runs from publication to drain. An idle engine
polls once more per window until it is back at S3. S3 draws no more than
S1 on a desktop load and runs at 2.4 times its clock; S4 and up cost
several times as much.

The accounting is policy::Governor, which the host policy test runs
directly: publication to drain, busy spans carried across windows, flights
dropped by recovery without a drain, the idle re-arm back to S3, and
thermal and user ceilings, including those below S3.

Paired on the J713, on battery with the backlight at half (idle 5 x 60 s,
three 60 Hz scenes 3 x 30 s each, the glmark2 suite 3 times): idle draw
7.32 -> 6.06 W, the GPU rail 1.09 W -> 27 mW. The 60 Hz build scene stays
at S3 with no missed frame, and the glmark2 score is 574 -> 573.

Signed-off-by: Erik Bourget <erik@ebourget.net>
[eryk: don't re-arm the idle poll on a failed engine: its poll returns
 before service_power(), so the demand never decays, and an engine that
 failed while idle above S3 re-armed its poll every 20 ms until reboot
 once anything kicked it; the host test's call-site check follows]
Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@erykwieliczko

Copy link
Copy Markdown
Member

Merging with one fix. This comment is from Claude Opus 5.5, working for Eryk, not from Eryk himself.

Rebased onto integration/aurora 4b948209c61e and force-pushed, as Erik agreed. The change carries an [eryk: …] note.

Changes

  • drm/asahi: g16: Govern the published performance state by utilization: after a fail-stop, poll() returns before service_power(), so the idle re-arm fired every 20 ms until reboot. It now requires firmware.recovery.failed().is_none().

Testing

  • This exact tree (integration/aurora with this whole batch) was built and booted on Eryk's J713.
  • glmark2-wayland --validate: 27 scenes pass, 0 fail, 6 have no reference.
  • The governor reads S3 at idle, S9 for the whole of a 45 s load, and S3 again afterwards, peaking at 64 °C. 227,554 jobs with no recoveries or hangs.

🤖 Written by Claude Opus 5.5 (in Claude Code) for Eryk.

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.

3 participants