Repository navigation
cpufreq: apple: verify T8140 transitions on the next request - #227
Conversation
[Medium impact] The PR appears safe to merge; no actionable new issues were found. SummaryT8140 now checks the previous frequency request before issuing another, rather than waiting after each write.
No actionable new issues were found.
|
5bb24cb to
0c4dceb
Compare
maralcbr
left a comment
There was a problem hiding this comment.
Thanks @DjDeveloperr, this is a nice win. Keeping the check in front of every write means an unexpected owner still never gets a second request, and the gpubench numbers and the sugov count are convincing. One fix before merge:
- The fast-switch callback reads the cached index twice.
apple_soc_cpufreq_fast_switch()(drivers/cpufreq/apple-soc-cpufreq.c:431and:434) programspolicy->cached_resolved_idxand then reads it again for the return value.cpufreq_set_policy()rewrites that cache when it resolves new limits (drivers/cpufreq/cpufreq.c:487,:2664-2672), and nothing serialises that against the scheduler path. If a QoS update lands in between, the cluster runs at one state whilepolicy->curand the frequency scale record another. Schedutil skips the callback while its own target is unchanged (kernel/sched/cpufreq_schedutil.c:125-139), so that can last until the next real change. The same helper already runs on the other SoCs, but this PR is what moves T8140 off the slow path, which programs and reports one captured index, and T8140 is the SoC whose fast die-temperature limit moves the QoS most often. Please look the index up locally:int idx = cpufreq_table_find_index_l(policy, target_freq, false);, then passidxtoapple_soc_cpufreq_set_target()and returnpolicy->freq_table[idx].frequency.target_freqis already clamped bycpufreq_driver_fast_switch()and the table is sorted. Reading the cache once would only fix the mismatch, not a pick made from someone else's resolution.
Smaller things:
apple-soc-cpufreq.c:243-244still says every transition is verified. The last request is now only checked when the next one is issued, and under a static governor or before the policy exits there may be no next one. Suggest: "each request is verified when the next one is issued".- With fast switch, a lower fast die-temperature limit takes effect on the cluster's next scheduler update rather than straight away from
sugov_limits(). That's fine on a busy cluster, but a CPU undernohz_full=orisolcpus=nohzcan go a long time without one. Worth a sentence near:110-112, where the sampling side already handles nohz_full. - Optional: keep the blocking wait on the
->target_indexpath and defer only in->fast_switch. It costs nothing there, and requests from suspend and the static governors would be checked when they're made.
CI was still running when I looked: compile, KUnit, package and both host-test jobs were pending, so I'll check ci / result again with the fix. For release (not the source merge): the Neo boot of the 7.2.11 build you mentioned, on the final head, with the sugov count and no DVFS errors.
| * back the last accepted request, not a measured frequency; every transition | ||
| * is verified, see apple_soc_cpufreq_set_target(). Voltage, PLL and firmware | ||
| * throttling configuration are left as the bootloader set them. | ||
| * is verified, see apple_soc_cpufreq_set_target(). Each request is checked |
There was a problem hiding this comment.
nit: "every transition is verified" is no longer quite true for the last request. Suggest: "each request is verified when the next one is issued".
There was a problem hiding this comment.
Fixed in 54b6358: the comment now says requests from the fast-switch path are verified when the next one is issued, and requests through ->target_index are still waited for.
Every T8140 frequency request spun until the cluster reported the new state, about 50 us per transition, and the same verification kept schedutil off the fast-switch path. Each change therefore woke the governor kthread, which runs above every normal task and then busy-waited on whichever CPU it landed on. Under light GPU work the P-cluster changes state about a thousand times a second: each GPU completion runs the mailbox interrupt thread, a real-time task that raises the cluster to its top state, and the cluster drops back once the thread sleeps. The spin cost 7% of a P-core and landed between a completion and the next submission. On the fast-switch path, check each request when the next one is issued instead. That request still waits for BUSY to clear and still stops all requests if the accepted state is not the one last written, so an unexpected owner is caught as before, one request later. With the wait gone the driver can switch from the scheduler path, and the governor thread is no longer used. Requests through ->target_index, from suspend and the static governors, still wait for their own transition. The fast-switch callback now resolves its table index from the frequency it is given instead of reading policy->cached_resolved_idx, which a concurrent limits update can rewrite between programming the cluster and reporting the frequency. Measured on Neo, same boot, alternating with the previous module (gpubench, us per submission, two rounds): compute, depth 2 58.8-59.7 -> 46.4-47.3 compute, depth 1 107.8-108.4 -> 102.6-102.7 render, depth 2 47.5-47.8 -> 45.8-45.9 render, depth 32 and compute, depth 64: unchanged (cherry picked from commit 9aada77) Signed-off-by: DjDeveloperr <hey@djdev.me>
0c4dceb to
54b6358
Compare
|
Thanks, all four are in 54b6358:
It builds cleanly on 7.2.11 with the Neo config. I'll post the Neo boot of the 7.2.11 build on the final head with the |
chriskearney
left a comment
There was a problem hiding this comment.
Checked 54b635838d4c32c4b52fb822b283d19dec8fca39: Marcelo's cached-index concern is resolved. Fast switch now resolves one local index from target_freq, programs it and returns that same entry's frequency. The ordinary target path retains blocking completion verification; only T8140 fast switch defers it.
This revision also changes the shared fast-switch helper used on M1/M2/M3, so those machines provide relevant regression coverage even though they cannot validate Neo's lazy-verification gains.
For Neo qualification, keep the final 7.2.11 boot and a suspend/resume cycle with no DVFS errors. The documented QoS-limit delay until the next scheduler update, particularly with nohz_full, remains a qualification limit rather than something a clean M3 run can settle.
Validation here: source review of the request/fast-switch paths and schedutil limits handling. No independent build or physical boot of this head yet; its new CI run is still pending.
maralcbr
left a comment
There was a problem hiding this comment.
Reviewed at 0c4dceb (source only; no build or boot on my side).
The motivation is clear and the numbers are good, but two things need changing before this can go in.
1. Fast-switch now spins in scheduler context. With fast_switch_possible set, apple_soc_cpufreq_set_target() runs from schedutil with the runqueue lock held and IRQs off. Its first step is still the readq_poll_timeout_atomic() waiting for BUSY to clear, with T8140's 400 µs timeout. Whenever a request arrives while the previous ~50 µs transition is still in progress (likely at ~1000 transitions/s), that wait moves from the sugov kthread into the scheduler hot path.
Simply returning early when BUSY is set doesn't work: schedutil stores next_freq before calling ->fast_switch(), ignores its return value, and suppresses an identical next target, so a skipped request would never be retried. Any skip needs explicit retry handling, or the overlap has to be bounded some other way.
2. Lazy verification also covers the slow and suspend paths. lazy_verify is checked inside the shared set_target(), so ->target_index() loses synchronous verification too. cpufreq_generic_suspend() stops the governor and then requests suspend_freq; nothing may follow to verify that request was accepted. Please keep synchronous verification on the non-fast-switch path (including suspend), and make only the fast-switch path lazy.
Also: the measurements were taken on 7.3, and the 7.2.11 Neo boot is still to come. Please post it with the result of a combined run with #226 and #228 on the same base, since all three affect the same utilisation and frequency loop.
|
Thanks both. This review was at 0c4dceb.
|
The previous change verified a fast-switch request only when the next one was issued. That request still spun on BUSY in the scheduler for up to the transition timeout, the last request of a burst was never verified, and a request dropped on BUSY was never retried: schedutil does not ask again for the frequency it last asked for. Verify from a hard-irq hrtimer instead, serialised with the scheduler's requests and ->target_index by a per-policy raw spinlock: - A request that finds the cluster idle checks that the accepted state is the one last written, and is written at once. The timer checks the last request written the same way within 1 ms, with 0.5 ms of slack so that it can share an interrupt, so every request is verified, the last of a burst included. - A request that finds the cluster busy is kept as pending; it is neither spun on nor dropped. The timer writes it once the transition has finished, from about when it should have, and verifies it in turn. - A transition still busy past the transition timeout, or an accepted state other than the one written, latches transition_failed as before. The timer reports it, outside the scheduler. - fast_switch returns the requested frequency once a request is committed. ->target_index cancels the timer, drops any pending request and keeps its blocking verification; ->exit cancels the timer. Only lazy_verify SoCs (T8140) use the timer. Measured on T8140 against the previous change, in the same boot, alternating, three rounds (us per submit, medians): compute.light queue d1 102.67 -> 102.38 compute.light queue d2 46.70 -> 46.61 render.light queue d2 45.81 -> 45.76 with 5-6% more timer interrupts over those rows. Under mixed GPU and CPU load the policies made about 570 transitions/s with no DVFS errors; all-core load throttled to the same frequencies; the static governors and a pm_test=devices suspend/resume cycle ran without errors. A diagnostic build, not part of this change, showed the timer alone finding a refused write when no further request was issued, and a stuck BUSY on both policies. Signed-off-by: DjDeveloperr <hey@djdev.me>
|
The fast-switch rework is in 7298b9b ("verify T8140 fast-switch requests from a timer"):
On Neo, against the previous head, same boot, alternating, 3 rounds:
|
maralcbr
left a comment
There was a problem hiding this comment.
Follow-up at 7298b9b (source only).
First, a correction: my previous review was posted against 0c4dceb after you had already pushed 54b6358, so the slow and suspend path point was already fixed when I posted it. Sorry for the noise.
The timer rework fixes the scheduler-context spin well. Keeping the latest request as pending means a suppressed identical target can't be lost, every write gets a verifier, including the last of a burst, and I found no lock-order problem (rq lock / sugov update_lock → priv->lock → hrtimer base). Re-arming the timer from its own callback is fine, and so is HARD mode on PREEMPT_RT.
One remaining issue: generic suspend can skip the driver. fast_switch() returns the requested frequency even when the request is only pending, and the core stores it in policy->cur (cpufreq.c:2209). If that frequency equals suspend_freq, cpufreq_generic_suspend() → __cpufreq_driver_target() takes the target_freq == policy->cur shortcut (cpufreq.c:2370) and never reaches set_target(). So the cancel, drain and synchronous verification are bypassed. Two ways to get there:
- A transition is BUSY, schedutil asks for the lowest frequency (stored as pending), and suspend arrives before the timer writes it.
- Without any tight timing: the timer finds the previous transition failed and drops the pending low request (
apple_soc_cpufreq_fail()clearspending_pstate). Hardware never gets it, butpolicy->curis already low, so a later suspend skips the driver and its failure check.
CPUFREQ_NEED_UPDATE_LIMITS alone wouldn't cover it, because __target_index() has its own equality shortcut. I'd suggest a driver ->suspend that always cancels the timer, drains any pending or in-flight request, verifies it synchronously, and reports a latched failure, then calls the generic suspend. Please exercise "pending at suspend" and "failed pending at suspend" (your diag build looks like the right tool).
Smaller points, not blocking:
- On lazy SoCs
set_target()holds the rawpriv->lockwith IRQs off across both BUSY polls, up to about 800 µs of polling budget. Afterhrtimer_cancel(), the governor contract already keeps fast switch out of->target_index, so the lock (and the "late fast-switch request" comment) could go, or be dropped around the polls. hrtimer_cancel()inset_target()can cancel a queued failure report, leavingfailure_unreportedset and never printed. Report it while draining.- With BUSY and a pending request, the timer re-arms every 4 µs until the 400 µs deadline: roughly 90 hard-IRQ callbacks for a stuck transition. Fine as a failure-path cost; just noting it.
->exitcancels the timer without draining, so "every write is verified" doesn't strictly hold across teardown. Worth a word in the comment.
Keeping changes requested for the suspend path only.
|
Thanks, and good catch on the
|
A fast-switch request returns its frequency once committed, and the core records it in policy->cur, but on a lazy_verify SoC it may still be pending behind a busy transition, or be dropped when the verify timer finds that transition failed. cpufreq_generic_suspend() skips a suspend frequency equal to policy->cur, so suspend could leave the cluster at an older state, or miss a failure, without calling the driver. Give the driver its own ->suspend: on lazy_verify SoCs it cancels the verify timer, reports a failure the timer had yet to report, writes the pending request and waits for it, verifying it and the transition before it, and only then does the generic suspend. A latched failure fails the driver suspend, which the core logs. Waited-for requests share the draining step. They never run alongside fast-switch requests, so ->target_index no longer holds the raw spinlock, with interrupts off, across its two polls; a failure report the timer lost to the cancel is now printed; ->exit drains as well. Measured on T8140 against the previous change's base in the same boot, alternating, three rounds (us per submit, medians): compute.light queue d1 102.25 -> 102.12 compute.light queue d2 36.93 -> 36.47 render.light queue d2 48.24 -> 48.45 A pm_test=devices suspend cycle ran without DVFS or driver suspend errors. With a diagnostic build, not part of this change: a request left pending for the lowest frequency, equal to policy->cur and suspend_freq, was written and verified at suspend; a stuck BUSY that dropped the pending request, with the timer's report withheld, was reported at suspend and failed the driver suspend. Signed-off-by: DjDeveloperr <hey@djdev.me>
|
Suspend path fixed in 979d4f1 ("cpufreq: apple: settle T8140 lazy requests before suspend").
Tested on T8140:
The combined 7.2.11 run with this head is in progress, and I'll post its results on #226, #227 and #228. |
|
Results from the combined 7.2.11 Neo run with this head (979d4f1), together with #226 and #228:
|
maralcbr
left a comment
There was a problem hiding this comment.
Approving at 979d4f1.
The driver ->suspend closes the gap: it runs after the governor has stopped, settles any pending request so the cluster matches policy->cur before the core's equal-frequency shortcut, and reports a withheld failure. The other points are addressed too: no IRQs-off polling on the slow path, the lost failure report is printed, and ->exit drains. The combined run is clean: pm_test freeze at devices and platform, and no DVFS lines across the whole run.
One check is deferred, not waived. Real s2idle doesn't resume on Neo yet, and the driver keeps expected_pstate across suspend without reconciling it on resume. That assumption predates this PR, but if firmware changes DVFS_CMD during a real s2idle, the first request after resume will latch transition_failed for good. Once real s2idle resumes on Neo, please check that both policies still change frequency under load afterwards (frequencies actually moving, not just a clean log) and post the result here. I'm tracking it on our side.
|
Thanks. Agreed on the s2idle check: once real s2idle resumes on Neo, I'll put both policies under load afterwards, confirm that their frequencies actually move, and post the result here. One more commit is coming to this branch. A review on the aurora-silicon twin found a gap: with fast switching, a lowered fast die-temperature limit reaches the cluster only at schedutil's next frequency update. A nohz_full CPU with its tick stopped may not have one for a long time. Before lazy verification, The fix is "cpufreq: apple: apply a lowered T8140 fast die limit at once". It builds with W=1 and is checkpatch-clean on 7.2.11. It's being qualified on Neo now:
I'll push it with those results, so the branch will need another look. |
The fast die-temperature limit is a FREQ_QOS_MAX request. T8140 now switches frequency from the scheduler, where schedutil only records a lower limit and applies it at its next frequency update, which a nohz_full CPU running a task with its tick stopped may not have for a long time. Through ->target_index, which T8140 used before lazy verification, the core applies the limit at once. When a sample changes the limit of a lazy_verify cluster, record the fastest P-state at or below it and move the cluster to the P-state the scheduler last asked for, at most that one, through the same locked path as fast-switch requests. Later fast-switch requests are clamped to it while the limit holds, and return the frequency actually requested. Going back to the scheduler's request also covers a limit that rises again before the QoS request was updated: the request is then left as it was, and schedutil does not ask again. Waited-for requests own the cluster from the drain to the next fast-switch request, and the core applies the limit to them, so the samples leave the cluster alone then. The drain now marks this before it cancels the verify timer, so that no sample queues the timer again. Measured on T8140 with a diagnostic build that forces a 2196 MHz limit while a spinner runs on a P-core: the limit is written inside the sample that sets it, where the scheduler path took about 1 ms, and 0.02 to 2.5 ms with nohz_full on that CPU. A drop and restore within one sample, with the QoS request unchanged, returns the cluster to the scheduler's request. Under a real thermal load the frequency stayed at or below the limit in every sample. Static governor switches, unbinding and a pm_test=devices cycle were clean, and light GPU submission costs are unchanged. Other SoCs are unchanged. Signed-off-by: DjDeveloperr <hey@djdev.me>
|
Pushed 29ce152, "cpufreq: apple: apply a lowered T8140 fast die limit at once", on top of the commit you approved. The commit fixes a gap a review on the aurora-silicon twin found. With fast switching, a lowered fast die-temperature limit reached the cluster only at schedutil's next frequency update, which a nohz_full CPU with its tick stopped may not have for a long time. Before lazy verification, Measured on Neo:
The full table is on aurora-silicon/linux#443. Only lazy_verify SoCs run the new code. |
maralcbr
left a comment
There was a problem hiding this comment.
Reviewed at 29ce152 (source only). This replaces my approval of 979d4f1.
Applying a lowered limit inside the sample is the right fix for the nohz_full gap, and most of it checks out: the fd->lock → priv->lock → hrtimer base nesting is valid on PREEMPT_RT, sampling runs from softirq, setting waited before hrtimer_cancel() stops a sample re-arming the timer during the drain, init sets up the lock and cap before apple_fast_die_init(), and T8140 opp-levels rise with frequency, so min() over P-states is sound.
One hole: lazy_cap() moves the cluster without updating policy->cur, which reopens the suspend shortcut.
- schedutil asks for H while the cap is L, and L equals
suspend_freq.lazy_request()clamps it and returns L, sopolicy->cur = Landasked_pstate = H. - The cap rises before the QoS work has applied the drop.
lazy_cap()restoresmin(asked, cap) = Hin hardware, butpolicy->curstays L, and schedutil, which already holdsnext_freq = H, won't ask again. - At suspend,
apple_soc_cpufreq_suspend()drains and verifies H, thencpufreq_generic_suspend()seessuspend_freq == policy->curand returns without calling the driver. The cluster suspends at H.
Two simple ways out: have the lazy suspend path write and verify the suspend P-state itself (via apple_soc_cpufreq_set_pstate()) instead of relying on cpufreq_generic_suspend(), or set policy->cur to the settled frequency before calling it. It would also help to keep policy->cur honest when lazy_cap() moves the cluster, since scaling_cur_freq and stats read it. Please exercise this sequence with your diag build (low cap equal to suspend_freq, cap rises, then suspend).
Everything else from the previous rounds still stands.
|
Thanks. The fix for the
It's being exercised on Neo now with the diag build, before and after the fix: limit at |
|
This merged before the |
Summary
Every T8140 CPU frequency request spun until the cluster reported the new state, about 50 µs per transition. Because of that verification, the T8140 also never used schedutil's fast-switch path. Every change therefore went through the governor kthread, which runs at deadline priority and busy-waited on whichever CPU it landed on.
Under light GPU work the P-cluster changes state roughly a thousand times a second. Each GPU completion is handled by the mailbox interrupt thread, a real-time task that raises the cluster to its top state, and the cluster drops back once the thread sleeps. The spin cost about 7% of a P-core, and it landed between a completion and the next submission.
This change checks each request when the next one is issued. That request still waits for BUSY to clear and still stops all requests if the accepted state differs from the last one written, so an unexpected owner is still caught, one request later. With the wait gone, T8140 enables fast switch and the governor thread disappears. Requests through
->target_index, from suspend and the static governors, still wait for their own transition. The fast-switch callback resolves its table index from the frequency it is given rather than readingpolicy->cached_resolved_idx, which a concurrent limits update can rewrite.Measurements
Neo (A18 Pro); same boot, alternating with the previous module; gpubench, µs per submission:
Checks on Neo
sugovcount 0). schedutil runs fast switch on both policies.Scope
T8140 only, behind a new
lazy_verifyflag. T8142 and T8152 keep blocking verification until they are measured.Base
The same change on 7.2.11; it applies unchanged. The measurements above were taken on Neo with the 7.3 kernel. This branch builds cleanly on 7.2.11 with the Neo config; a Neo boot test of the 7.2.11 build follows.