Repository navigation
cpufreq: apple: verify T8140 transitions on the next request - #443
Conversation
|
Current head 6f5d208 adds direct fast die-temperature cap delivery and suspend draining after the tested e2a2d8e head. Those changes address the area covered by the earlier QoS dispatch finding; that finding is historical and is not a verdict on this new implementation. The earlier final-request issue was already addressed by e2a2d8e's timer. The completed results below are for e2a2d8e only. On integration/aurora at 421e314 with shared #438 and #456, DCP is PASS (63/63). KUnit has 905 passes and 1 policy skips. Universal WERROR is PASS; Fedora-base WERROR is PASS. The actual-function timer and QoS controls also target e2a2d8e. These passes do not qualify 6f5d208. The new head needs fresh DCP, KUnit, universal and Fedora-base builds, plus controls for direct cap delivery without a scheduler update, cap relaxation, BUSY pending requests, suspend and teardown. Hardware qualification remains with the orchestrator. No current-head failure or hardware result is claimed. |
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
9aada77 to
b11a5e9
Compare
|
Thanks, agreed: a final deferred request with no successor must be verified too. Since your comment I've split the paths:
The Neo smoke covers boot, load and thermal transitions, an injected refused transition, and a final request without a successor. I don't have an M1 Pro, and the M2 here is the build host, so I'll say explicitly what that regression coverage relies on. |
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.
|
Your point is addressed in e2a2d8e. A per-policy hard hrtimer now verifies the last written fast-switch request within 1 ms, so a final request with no successor is checked, and a refused write or stuck BUSY latches On Neo: the light-GPU rows match the previous head (compute d2 46.70 → 46.61 µs), with 0 DVFS errors over about 570 transitions/s. A diag build showed the timer alone catching both a refused write with no later request and a stuck BUSY. Governor switches and a |
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.
|
Follow-up commit b7ea3ca, "cpufreq: apple: settle T8140 lazy requests before suspend", is the same change as omacom#227. A fast-switch request can still be pending when the system suspends. It passed a |
|
Thanks, agreed. Before lazy verification, T8140 used I have a fix that is built but not yet tested on hardware: "cpufreq: apple: apply a lowered T8140 fast die limit at once".
Hardware qualification on Neo is next: forced-limit delivery with and without the fix under a pinned spinner, the same on a |
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.
|
Cap delivery fixed in 6f5d208, "cpufreq: apple: apply a lowered T8140 fast die limit at once", and qualified on Neo. Setup: 7.3 perf kernel; "before" is e2a2d8e + b7ea3ca. A diagnostic build, not part of the change, forced the limit and timed the gap from the sample that changes it to the first command write at or below it.
The nohz_full boot didn't produce an unbounded delay before the fix. cpu0 shares the policy with cpu5 and stays the housekeeping CPU, and the QoS work's kworker on cpu0 triggers a schedutil update. The fully unbounded case needs every CPU of the policy tickless, which this boot can't do. The fix doesn't depend on any of that: it delivers inside the sample, which runs at most every 5 ms while the limit is below the maximum and once a tick otherwise, whatever the governor does. The drop-and-restore row covers a case I found while reviewing my first version. If the limit falls and recovers before the QoS work runs, the QoS request never changes, so schedutil never asks again. The driver therefore moves the cluster to the scheduler's last request, at most the cap, and raises it again too. M1 Pro and M2: |
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.