Skip to content

drm/asahi: g17: admit a context's ioctls to work states in submission order (stacked on #275) - #276

Merged
scottjones merged 1 commit into
integration/aurorafrom
aurora/g17-in-order-admission
Oct 31, 2026
Merged

scottjones merged 1 commit into
integration/aurorafrom
aurora/g17-in-order-admission

Conversation

@DjDeveloperr

Copy link
Copy Markdown
Collaborator

Mirror of aurora-silicon#571, cherry-picked with -x onto #275's branch (aurora/g17-work-state-engine-reserve), so this PR is stacked on #275; it applied without conflicts. The touched files (g17/context.rs, g17/context/identity.rs, g17/runtime.rs) are identical on both trees. KUnit asahi_g17* passes 132/132 on this tree. The hardware results below were measured on the aurora build.

This fixes the GPU stall that AsahiLinux#570 left open ("Still open"), tracked in aurora-silicon/gpubench#5. Under deep mixed render and compute load on one queue, every completion of a G17 context stops within seconds.

What native does differently

The comparison ran natively on the same Neo (A18 Pro, macOS 26.6.2, same GPU firmware), using a Metal copy of queue.depth:

  • one MTLCommandQueue;
  • 128 compute command buffers (one 4×64 chain dispatch each) and 64 render command buffers in flight;
  • each render clears a 64×64 target, draws a scissored triangle and copies the target to a buffer;
  • the copy is a blit, or in one variant an explicit compute kernel, which matches hk's render-then-compute ioctl.

Every native run passed:

Run Result
15 s each: blit copy, compute-kernel copy, untracked resources, default queue limit 0 errors, 0 GPU recoveries; 45–49k command buffers/s
60 s 2.76M command buffers, 0 errors
two processes at once, 15 s both passed

Per-command-buffer scheduled/completed handler times, kernel and GPU timestamps, and a long head command buffer with 191 behind it show how Metal schedules:

  • At most 36 command buffers per queue sit between kernel scheduling and completion. That holds for compute-only, render-only and mixed loads, and behind compute or render heads. Two queues hold 72. commit() never blocks; the rest wait in the driver.
  • Command buffers are scheduled in commit order across engines and complete in order.
  • There is no render/compute ordering beyond that. Compute runs while an earlier render is still executing, and vice versa.

Linux has no such order. Each engine admits a context's ioctls in submission order, but render and compute advance independently. Render admission takes work-state nodes for ioctls far ahead of an older ioctl still waiting on the compute queue.

Fix

An ioctl takes its work-state node only while at most 8 older ioctls of its context have neither taken one nor left without one: ordered admission across engines with a bounded run-ahead.

Strict order (run-ahead 0) is the closest copy of native, and it passes the repro too. Native schedules at commit on one thread, though. Here each engine admits from its own scheduler, so a strictly ordered turn costs a scheduler round trip whenever the other engine's ioctl is ready first. That cost showed up in a windowed Vulkan client (below).

  • The turn is tracked under a new per-context mutex, taken inside the work-storage lock.
  • Ioctls that fail or are cancelled before their turn are remembered within 1024 ordinals of it. One that leaves further ahead makes that context's admission unordered (the old behaviour) instead of stalling it.
  • A refused publisher waits on its engine's admission domain. The turn moving past a refusal wakes both domains. Contexts that are never refused take no extra wake-ups.
  • USB hub (QinHeng 1a86:809d) becomes unresponsive after suspend/shutdown on MacBook Air M1 AsahiLinux/linux#570's last-node rule stays.

Evidence (Neo, diagnostic knobs, queue.depth c128+r64 on one queue)

Admission rule Result
stock (AsahiLinux#570) stall within 0.5–4 s; reproduced again as the same-boot control below
live nodes capped at 32/48/56 (from AsahiLinux#570) stall
window 36 over unretired ioctls only (native depth, no order) GPU firmware crash at 0.25 s
window 36 over node-holding ioctls only firmware crash at 7 s
strict order 30 s pass, every run (6 runs incl. the one with a 36 window)
order + run-ahead 1, 2, 4 30 s pass
order + run-ahead 8 30 s pass, 5 of 5 (+ final qualification below)
order + run-ahead 16 30 s pass
order + run-ahead 32 stall at 6.6 s

The run-ahead is empirical. 16 is the largest value that passed, and 32 the first that failed, so 8 leaves a factor of two.

Qualification

Rows Delta
render.light tri d2/d4/d8/d32/d64 (5 rounds) −0.5 / −0.5 / −1.2 / −0.8 / −2.4% (d1 is bimodal in both arms)
compute.light d1/d8/d64 −0.9 / −2.1 / −6.6%
render.heavy fill 1080p d2/d8, copy.bw fill d1/d4, compute.heavy alu d16, render.light queues d4/d8, concurrent.cr interleave/queues, sync.signal within ±0.5%
rt.scene preset=solarbay (game-like frame: AS refit + RT + raster, two frames in flight) ±0.0%
vkmark through Hyprland (windowed, mailbox), 10 rounds score +0.2%, shading −0.3%, desktop +0.1%

Strict order (run-ahead 0) left vkmark's shading scene about 9% slower in 4 of 11 rounds. Run-ahead 8 showed that once in 23 rounds, and its dedicated 10-round A/B above was a tie.-REGRESSION-PASS`.

  • Targeted CTS (synchronization2 basic/timeline, api.command_buffers, render-pass subsets): 4687 Pass, 0 Fail, 681 NotSupported, the same as USB hub (QinHeng 1a86:809d) becomes unresponsive after suspend/shutdown on MacBook Air M1 AsahiLinux/linux#570.
  • Full synchronization2 group: 36516 Pass, 0 non-pass, the same as stock.
  • queue.depth c128+r64 repro: 4 × 30 s pass, plus a 60 s run that completed about 2M checked submissions with 0 errors (the harness's own 60 s timeout cut its verdict line). No kernel GPU lines during any repro run.
  • Earlier boots: 5 × 30 s pass with strict order, and 5 × 30 s pass at run-ahead 8.
  • Stampless retirements: the full synchronization2 group retires about 9.7k compute commands on the 50 ms stampless fallback both on stock (lifetime counter 9728) and with this change (9856; the counter prints in steps of 64). This is the pre-existing CTS behaviour noted in USB hub (QinHeng 1a86:809d) becomes unresponsive after suspend/shutdown on MacBook Air M1 AsahiLinux/linux#570, unchanged here.
  • Control: contexts created with the old unordered admission still stall on the same boot (6.3 s and 13.1 s in two sessions).

Performance

Same-boot A/B on the Neo, with this change vs contexts created with the old unordered admission (a diagnostic build only), arms rotated, quietdesk on. Medians, ordered vs unordered:

Rows Delta
render.light tri d2/d4/d8/d32/d64 (5 rounds) −0.5 / −0.5 / −1.2 / −0.8 / −2.4% (d1 is bimodal in both arms)
compute.light d1/d8/d64 −0.9 / −2.1 / −6.6%
render.heavy fill 1080p d2/d8, copy.bw fill d1/d4, compute.heavy alu d16, render.light queues d4/d8, concurrent.cr interleave/queues, sync.signal within ±0.5%
rt.scene preset=solarbay (game-like frame: AS refit + RT + raster, two frames in flight) ±0.0%
vkmark through Hyprland (windowed, mailbox), 10 rounds score +0.2%, shading −0.3%, desktop +0.1%

Strict order (run-ahead 0) left vkmark's shading scene about 9% slower in 4 of 11 rounds. Run-ahead 8 showed that once in 23 rounds, and its dedicated 10-round A/B above was a tie.

Head-of-line microbench (gpubench-stress diagnostic): queue 0's head ioctl waits on ~5 ms of queue-1 work while 4 independent ioctls of the other engine are submitted behind it. In both directions, the independent ioctls finished after the long work in 0 of 60 loops with ordered and unordered admission alike. Queue order before admission already serializes them on this stack, so ordered admission adds no head-of-line wait.

… order

Each engine admits a context's ioctls in submission order, but the
render and compute engines advance independently. Under deep mixed load,
render admission takes work-state nodes for ioctls far ahead of an older
ioctl that is still waiting for its turn on the compute queue.

gpubench-stress queue.depth keeps 128 compute and 64 render submissions
in flight on one queue, each render followed by a compute copy. On a Neo
(A18 Pro), every completion of the context stops within 0.5-4 s: compute
and render-pair stamps stop together, newly published compute never
runs, and the client loses the device. Capping the context's live nodes
at 32, 48 or 56 does not prevent this.

Native macOS on the same machine and firmware runs the same load (one
MTLCommandQueue, 128 compute and 64 render command buffers in flight,
each render followed by a compute copy) for 60 s without an error. Metal
schedules a queue's command buffers in commit order across engines, with
at most 36 between scheduling and completion.

An ioctl now takes its node only while at most 8 older ioctls of its
context have neither taken one nor left without one. Native scheduling
happens at commit, on one thread. Here each engine admits from its own
scheduler, so a strictly ordered turn costs a scheduler round trip
whenever the other engine's ioctl is ready first; that slowed a windowed
Vulkan client. Under the queue.depth load, run-ahead limits up to 16
held and 32 did not, so 8 leaves a factor of two.

The turn is tracked under a context mutex taken inside the work-storage
lock. Ioctls that fail or are cancelled before taking a node are
remembered within 1024 ordinals of the turn. One that leaves further
ahead makes the context's admission unordered, as before, rather than
stall it. A refused publisher waits on its engine's admission domain,
and the turn moving past a refusal wakes both domains. Contexts whose
ioctls are never refused, such as single-engine ones, take no wake-ups
from this.

The last-node rule stays: render ioctls still hold nodes while their
copies wait in the compute queue.

Link: aurora-silicon/gpubench#5
Signed-off-by: DjDeveloperr <hey@djdev.me>
(cherry picked from commit c510e9d)
scottjones
scottjones previously approved these changes Oct 31, 2026

@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 1d3726b on #275's branch, and ran it on a Neo with #278 and #275.

The rule does what the body says. We looked for deadlocks, lost wakeups and fairness problems with #275's nodes and found none:

  • Ordinals and job pushes both happen under the queue lock (file.rs:1448, g17/queue/mod.rs:462), after the in-syncs are resolved, so an ioctl can't wait on a fence from a later one.
  • At most 9 ioctls at or past the turn hold nodes, and there are 63, so order never refuses the turn itself. An exhaustive small model found 0 deadlocks.
  • The domain is sampled before acquire (g17/runtime.rs:1859, g17/runtime/render/submit.rs:163), so a refusal can't sleep through a turn move.
  • A refused render holds no pair yet (submit.rs:161, then 178), so it can't block another client through a slot.

Three things we'd like looked at, none blocking:

  • A compute command refused by the turn looks like it spins. prepare_compute queues a housekeeping pass and waits on progress (g17/runtime.rs:1863, 1865), each pass notifies progress (g17/runtime.rs:2349), and that reruns prepare. Each round takes the device mutex. We sampled worker CPU during queue.depth: about 130-180% of a core across G17 and event workers in the deep phase, none above 61%. That doesn't clear it, because top can't isolate this loop and a wait-based loop wouldn't pin a core. Can you check it with your counters? If it's real, skipping queue_events for a turn refusal should fix it.
  • The unordered fallback is one-way and silent (g17/context/identity.rs:154), and a client can reach it by failing an ioctl after publish (g17/queue/mod.rs:205). We'd add a dev_warn_once.
  • g17/context.rs:452 calls release_state() with work and order held, the same note as on #275. compute.light d64 at -6.6% is worth repeating, and the "Regression gate" line in the body has the table pasted into it.

The mirror is faithful: the patch is byte-identical to c510e9d. Scope is g17 only, so T8140 and T6040, and RUN_AHEAD 8 was only measured on A18 Pro firmware. We'd expect any T6040 cost to be throughput rather than a stall, but we'd like one run before Mesa admits that chip.

We ran 2dd4aab + #278, #275, #276 and the five-PR build with #264 and #277, each through a private Vulkan driver with omacom/mesa#20: queue.depth 5/5 and sparse.churn 3/3 at 60 s, no flagged kernel line. The 7.2.11-10-1 release lost the device in its first queue.depth at 6.4 s, then had no GPU until reboot. That shows the set survives on omacom's tree. Isolating this change still rests on your same-boot unordered control. The limits: one job per kernel, one Neo, our clang 22 build, no T6040, no CTS on the three-PR build. Throughput also fell across the five runs on both builds (mixed 75k to 67k here), possibly thermal; we didn't check.

For the RC, this is more change than we'd normally want this late, but #275 alone is worse. Land it with #275, no release between them, after #278. Its base is #275's branch, so it needs merging there or retargeting.

@scottjones
scottjones changed the base branch from aurora/g17-work-state-engine-reserve to integration/aurora October 31, 2026 05:04
@scottjones
scottjones dismissed their stale review October 31, 2026 05:04

The base branch was changed.

@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.

Approving again at the same head: retargeting from #275's branch to integration/aurora dismissed the earlier approval. Our review above stands.

@scottjones scottjones closed this Oct 31, 2026
@scottjones scottjones reopened this Oct 31, 2026
@scottjones

Copy link
Copy Markdown
Collaborator

What happened here, for the record. #278 and #275 merged this morning through the queue. This PR was based on #275's branch, so I retargeted it to integration/aurora. That dismissed the approval, so I approved again at the same head (1d3726b). No CI had ever run on this PR, because CI only runs on PRs aimed at integration/aurora, and the retarget didn't start it, so I closed and reopened the PR to trigger it. No code changed. It goes in the queue once ci / result passes.

#275 is now in integration/aurora without this, and its description says it can turn a client timeout into a GPU stall alone, so this should land before the next linux-aurora pin.

@scottjones
scottjones added this pull request to the merge queue Oct 31, 2026
Merged via the queue into integration/aurora with commit 360cf6a Oct 31, 2026
14 checks passed
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