Skip to content

drm/asahi: g17: count the stamp fallback from the drain of the command's node (stacked on #264) - #277

Open
DjDeveloperr wants to merge 1 commit into
neo/g17-render-stamp-fallback-20261028from
neo/g17-stamps-after-node-drain-20261029
Open

DjDeveloperr wants to merge 1 commit into
neo/g17-render-stamp-fallback-20261028from
neo/g17-stamps-after-node-drain-20261029

Conversation

@DjDeveloperr

@DjDeveloperr DjDeveloperr commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Mirror of aurora-silicon/linux#572 (merged as aeae3d9), cherry-picked with -x onto #264's branch (neo/g17-render-stamp-fallback-20261028), so this PR is stacked on #264. It applied without conflicts. KUnit asahi_g17* passes 130/130 on this tree, including both new cases. The hardware results below were measured on the aurora build.

This also answers the visibility question on #264: the 50 ms fallback fired thousands of times per synchronization CTS pass because it retired members of ioctls whose work-state node the firmware was still running. With this change it no longer does.

One CTS synchronization2 pass retires about 9.8k G17 compute commands on the 50 ms stampless fallback ("lifetime total N compute commands retired on their completion record after 50 ms without a completion stamp"). That happens on stock and with aurora-silicon/linux#570/AsahiLinux#571. A synchronization pass retires about 9.1k more. These stamps aren't missing. The firmware stores them later than the fallback assumed.

Cause

The firmware stores the completion stamps of an ioctl's commands only after every kick counted on the ioctl's work-state node has completed. A command whose record (compute) or pass record (render) is already present waits for its stamp for as long as the rest of its ioctl runs.

The 50 ms bound was measured from the record, so it retired those members on their records while the firmware was still running their node. It released their fences, VM pins and render leases before the post-maintenance stamp that the retirement path otherwise requires. This is the visibility window chriskearney asked about on #264. In these cases it wasn't a stalled stamp: the node was still running.

Evidence from a diagnostic build (this PR's base plus report tracepoints). It covered one full 6-worker synchronization2 pass (36516 pass, 0 non-pass), with the work-state node read at every fallback:

compute fallback retirements 9792
... whose node still had kicks running (completion word below the host count) 9792 (100%)
... whose stamp covered them later 9430 of 9430 tracked; 4.3 / 27.6 / 86 / 299 ms after the fallback (p10 / p50 / p90 / max)
render pass-record fallbacks in the same pass 6, all with their node still running
  • Node layout as read: completion word at 0xc, which is the sum of per-engine counts at 0x14/0x18/0x1c. Those counts are cleared when the node drains.
  • The most common shapes were:
    • one compute and one render in one ioctl, where the render hadn't finished (node of 3 kicks);
    • signal_order's single command buffer of twelve read operations, with the compute copies between them (12 renders + 13 computes = 37 kicks), where each render waits on the other queue.
  • Host traces of the stock kernel agree: every fallback retirement happened while another command of the same context was still executing. In more than half of the runs of consecutive fallbacks on a queue, the queue's stamp moved for the rest of the run within 0.2 ms of the last fallback.

The fallback added no latency here. The ioctl's out-fences wait for all its members, so they signalled at node completion either way. A synchronization2 pass takes 3 min 47 s with or without this change.

Fix

Count the bound from the drain of the command's node instead of from its record:

  • Once the 50 ms have passed, a compute command or render group whose stamps lag asks its node whether every counted kick has completed (WorkStateLease::drained(): node completion word ≥ host count).
  • If not, its stamps aren't late, and the wait starts again from now. The render pair stays latched for the 1 ms rescan, as before.
  • A drained node whose stamps still lag after the bound keeps the existing fallback.
  • A node that can't be read keeps the old bound.

The node is read only when the bound has already passed, so the common path is unchanged.

What a stampless retirement releases, and when that can still happen

Retirement (Deferred::Retired → Completion::complete() / release_after_retirement()) does the following:

  • signals the command's member fence (the ioctl's out-fence once all members are done);
  • publishes its timestamps (Output::publish());
  • releases its input sources, its VM job pin (vm_job) and, for a render, its render-slot lease;
  • for compute, frees its slot in the queue's 16-entry FIFO;
  • for a render, completes its free-list/QoS accounting and retires its slot memory.

With this change none of that happens on the fallback while the command's node still runs. A stampless retirement now needs one of two things:

  • the node has drained and the stamps still lag for 50 ms; or
  • the scheduler watchdog has expired an executed, consumed render group (expire_stamp_wait(), which sets the latch to 0). That one is unchanged and doesn't consult the node.

The aurora-silicon/linux#555 stall is still covered

The original render stall (aurora-silicon/linux#555) had the stamps stop one kick short while the group and later groups had executed, and no later event moved them:

  • If that node has drained, the stamps lag past the bound exactly as before, and the group retires on its pass record after 50 ms.
  • If that node never drains, the group no longer retires at 50 ms. The watchdog's last resort still retires it on its pass record at expiry instead of failing the client. The KUnit cases cover both paths.

No hardware repro of a drained-node stall exists today. In this PR's qualification boot, nothing retired stampless: no render or compute fallback line at all, including the first heavy CTS load after boot, where aurora-silicon/linux#555's stall used to show up once per boot.

Qualification

Performance

Same-boot A/B on the Neo. A diagnostic twin of this change has a knob that restores the bound from the record (the old behaviour). Arms were alternated with rotated order, 6 rounds, quietdesk on. Medians, with the change vs the old bound (positive = faster with the change):

Rows Delta
render.light tri d1/d2/d4/d8 −0.2 / −0.2 / −0.3 / −0.4%
render.light tri d32/d48/d64/d128 −0.8 / −1.2 / −0.8 / −1.1%
compute.light d1/d2/d4/d8/d64 −0.3 / −1.1 / +0.3 / −1.3 / +1.0%
render.heavy fill 1080p d1–d8, copy.bw fill d1–d8, compute.heavy alu d16 −0.4…+0.1%
render.light queues d4/d8, sync.signal timeline, concurrent.cr interleave/queues −0.1…+0.3%

All of this is noise. The two arms run the same instructions on these rows: the node is read only once a stamp wait has passed 50 ms, and no stampless line appeared during the A/B (the harness stops at the first one). Every median with the change lies within the old arm's own spread over its rounds (render.light d32: old 26.76–27.85 µs, with the change 27.44; compute.light d8: old 15.11–15.53 µs, with the change 15.37). The cross-module regression gate above passed as well.

…d's node

The firmware stores the completion stamps of an ioctl's commands only once
every kick counted on the ioctl's work-state node has completed: a node's
completion word reaches the host's submitted count, and only then do the
compute and render stamps move. A command whose record or pass record is
present therefore waits for its stamp for as long as the rest of its ioctl
runs. A command buffer of a dozen render passes and their copies, or a
compute command beside a render that waits on another queue, keeps the
stamps of its early members back for tens to hundreds of milliseconds.

The 50 ms stampless fallback measured from the record, so it retired those
members on their records while the firmware was still running their node:
their fences, their VM pins and their render leases were released before
the post-maintenance stamp the retirement path otherwise requires. On the
Neo, one CTS synchronization2 pass retired about 9.8k compute commands this
way. Each of them had kicks of its own node still running (the node's
completion word below the host count), and the stamp covered it 4 to 300 ms
later (median 28 ms).

Count the bound from the drain instead. Once it has passed, a compute
command or a render group whose stamps lag asks its node whether every
counted kick completed; if not, the stamps are not late and the wait starts
again. A drained node whose stamps still lag keeps the existing fallback,
so a group whose stamps stop one kick short still retires on its pass
record, and the watchdog's expiry still retires an executed group without
waiting for the node. A node that cannot be read keeps the old bound.

Signed-off-by: DjDeveloperr <hey@djdev.me>
(cherry picked from commit effb9e0)

@scottjones scottjones left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We read this at 7eaf9b6 on #264's branch, and ran it on a Neo with #278, #275, #276 and #264.

This closes the common case from #264. Compute (g17/queue/compute.rs:1048) and both render checks (g17/queue/render.rs:1065, 1328) now ask the node before retiring without stamps, and only published kicks count, so a full ring can't wait on itself. Our small model found 0 new deadlocks. We want this with #264.

What keeps this a COMMENT is recovery. Replay publishes again and adds to the node's count (g17/queue/compute.rs:2189, g17/queue/render.rs:844). From source we can't tell whether firmware counts a kick dropped by recovery on the completion word. If it doesn't, an innocent client's node never drains after another VM's recovery. Compute would then fail its VM through timeout (g17/queue/compute.rs:1811) and render would wait for the watchdog, where the old bound rescued both. None of our runs or your qualification exercised recovery. A run with an innocent client beside a forced hang, showing the node drains, or a fallback when it doesn't, is what would get our approval here, and on #264 with it.

Worth fixing or noting, not blocking:

  • The bound doesn't count from the drain, despite the comments at g17/queue/compute/retirement.rs:75 and g17/queue/render/retirement.rs:159. The first sample that sees a drained node retires at once (compute/retirement.rs:116, render/retirement.rs:197), so a drain just before it retires after 0 ms. A compute command behind the front can be past the bound on its first sample (g17/queue/compute.rs:934). A one-bit latch restarting the wait on the first drained sample would make the comments true.
  • expire_stamp_wait still skips the node (g17/queue/render.rs:1108). A group that used to leave at 50 ms now reaches that whenever its ioctl outlasts the watchdog, with #264's full exposure. We'd like a separate log line for retiring with the node still running.
  • The comment at compute/retirement.rs:84 ("cannot fail its VM") is no longer true.
  • The 1 ms poll now lasts as long as the sibling kicks run (g17/queue/compute.rs:871, g17/runtime/events.rs:513). No A/B row has a long job beside a short member.

T6040: g17/context.rs:92 reads a completion word only observed on 26A428. If it doesn't advance on 25G83, drained() is false forever and the fallback is gone. Mesa at 18ff4cdc9a refuses that chip today, so we'd want one run reading the word before that changes.

The mirror is faithful: the patch matches effb9e0 except hunk offsets and one context line.

On a Neo with our clang 22 build: CTS synchronization and synchronization2 0 fail with no stampless retirement, against the 9.1k to 9.7k compute retirements the AsahiLinux#570 and AsahiLinux#571 bodies report without this. Then queue.depth 5/5 and sparse.churn 3/3 with omacom/mesa#20. The three-PR build without this also logged none under those stress runs, so only the CTS result speaks to this gate. One job per kernel, one Neo, and it tests the set rather than this PR.

For the RC, fold this into #264 or land it directly after, with no release between them, and after #278 and #276.

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.

2 participants