Skip to content

drm/asahi: G17 stability fixes from linux-aurora PR #151 (7.3 port) - #390

Merged
DjDeveloperr merged 9 commits into
integration/aurorafrom
neo/g17-stability-7.3
Oct 23, 2026
Merged

DjDeveloperr merged 9 commits into
integration/aurorafrom
neo/g17-stability-7.3

Conversation

@DjDeveloperr

Copy link
Copy Markdown
Contributor

This ports the G17 stability fixes from omacom/linux-aurora#151 (qualified on Neo on 7.2.10) to integration/aurora (Linux 7.3).

I triaged each commit by code. integration/aurora was still exposed to two bugs reproduced on hardware, so those ports go first:

  • Order G17 queue owner changes after scheduler releases (render and compute). A same-VM successor could rebind a predecessor's pair in place while that context's release was published but not yet consumed, which crashed the GPU firmware and took Hyprland with it. 7.3 rebinds through the same reusable() check, with no scheduler-release check.
  • Keep G17 render kick ring storage for the life of the device. Reclaiming an installed but unpublished pair freed its ring while the client VM still aliased the address, so the next alias there failed with EFAULT and the client lost its device.

Also ported:

  • devres node tracking and the reduced target-record minimum (both from PCI: apple: enable experimental J700 radio endpoint bootstrap #151);
  • capacity waits no longer stop the runtime. On 7.3 any 8 timeouts in 30 s, even for packets that were never published, lost the device for good; unpublished-packet timeouts now return without counting;
  • wait for the contexts of closing queues at queue creation (with ERESTARTSYS and a refusal counter);
  • publication waits kept out of the submit ioctl;
  • two independent perf commits: rerun the event pass for completions missed while a pair is borrowed, and reuse compute timestamp pages.

Not ported:

  • fdinfo and accounting: 7.3 has its own design.
  • Compositor forfeit and per-graph view records: 7.3 gives each owner one pair and never takes a pair from a live owner.
  • The rest of the perf series: follow-up.

Tests:

  • KUnit under KVM with lockdep and sleep-in-atomic checks: asahi 67/67, Rust DRM 15/15.
  • All 14 G17 host resource tests pass, plus a new owner-release-order test whose negative controls fail as expected.
  • Largest frame grows from 10,064 to 10,112 bytes.

Neo hardware qualification (7.3.0-1-73fix-ARCH): in progress. It boots clean, with no WARN, BUG, coprocessor crash or runtime stop. The full stress battery and the Chromium real-application mix are running; results will follow here, and the PR stays a draft until they're in.

Rust Devres drops remove an exact node, but devres_node_remove scans the
entire device list to find it. Repeated removal of older GPU SG-table
resources therefore does quadratic work while holding devres_lock. After
large concurrent GPU workloads this keeps exiting clients CPU-bound for
roughly a minute.

Track the device-list owner in each node and remove a matching node
directly under the device lock. Clear membership on every detach path,
including replacement, ordinary and action removal, group removal, and
moves to the private release-all/group callback list. A concurrent drop
must leave nodes already owned by that release path alone. Preserve
callback ordering and lifetime handling.

Use single-copy membership accesses so a simultaneous comparison using a
different device does not race through an ordinary pointer access. The
membership field adds one pointer per resource node.

Source-derived ASan/UBSan controls cover the removal APIs, replacement,
open/closed/nested groups, private-list ownership and 1000 concurrent
removal/detachment/different-device lookup races. Retaining stale private
list ownership makes the negative control fail.

The combined series passes Neo G17P concurrent GPU stress, expiry cycles
and retained-VM context churn. Four clients performing 30 seconds of
checked compute work finish and exit in 31.46 seconds total, versus 86.97
seconds with the same compact target layout before this correction.

Signed-off-by: DjDeveloperr <hey@djdev.me>
The target-record stride already accounts for its computed layout and UAT
alignment, but its 256 KiB minimum wastes backing on smaller targets. Use
one 32 KiB TVB page as the minimum instead, keeping the computed used size,
consumer bounds, growth handling and all sixteen generations unchanged.
Queue capacities, batching and synchronization policy are unchanged.

Source-derived controls validate 388800 layouts against the original
capacity and consumer bounds; 114426 need less backing. At forty held GPU
clients with 124 target objects on Neo, allocation extents fall from
272236544 to 44695552 bytes, saving 217 MiB for that workload.

The compact layout passes changing-pixel and layered readbacks, target
growth, concurrent render/compute/mixed stress, Vulkan multisample checks,
compute pool expiry and 2000 retained-VM contexts. Brilliant Diamond also
reaches its language menu around 30 FPS in a bounded emulator run; broader
gameplay and application performance validation remain separate.

Signed-off-by: DjDeveloperr <hey@djdev.me>
When a logical queue closes, its scheduler release is published once its
work is idle. The firmware consumes the release against the queues that
scheduler last ran. After the host has seen it consumed, the teardown parks
the scheduler's render pair, and a later owner reinstalls a parked pair
with fresh queue records.

A successor on the same VM can find such a pair still installed after the
release was published but before its consumption was seen. The pair is
then reusable() and not foreign, so build_render_pair() rebinds it in
place, rewriting the job-list head, pid and scheduler fields of queue
records the firmware ran for the released scheduler, and publishes its
first command, which carries the owner change.

A GL client that recreates its context every few milliseconds next to
three Vulkan clients slows the release enough to open this window. On the
7.2.10 line its successor published the first commands inside it and the
GPU firmware crashed about 3 ms later without writing a crash log. At the
crash the release had been consumed, the pairs had been rebound in place
and their first commands for the new owner were still in flight. The 7.3
rebind path keeps the same reusable() test and has no release check.

Order owner changes and scheduler releases:

 - A render pair whose context has an armed or unconsumed scheduler
   release is not rebound. The new owner retries until the teardown has
   parked the pair, then installs it with fresh records. The check is
   made when the rebind is chosen and again when it is committed.
 - A scheduler release waits while a render pair or compute queue that
   changed owner away from that context still has the new owner's first
   command in flight. A pair records at publication which scheduler its
   queues last ran for. A borrowed pair keeps a snapshot of that state.
 - The first command after an owner change waits while the scheduler the
   pair last ran for is being released.
 - The same holds for compute queues: a queue whose bound scheduler has an
   armed or unconsumed release is not rebound or re-armed for another
   owner, and the first command after a compute owner change (including a
   queue an idle live owner yielded through share_idle()) waits while the
   scheduler the queue was bound to is being released.
 - A pair parked without a consumed release keeps the scheduler it last
   ran for, so its next owner change still waits for that release. A park
   after the consumed release of the pair's scheduler clears it.
 - A publication refused because its pair is busy kicks the event worker
   on every poll, since the event worker observes the consumed release and
   parks the pair, and gives up after the job timeout instead of after
   5000 polls.

The normal path is unchanged. A pair whose owner does not change compares
one word at publication. A release is delayed only while an owner change
is in flight. After a context churn, a same-VM successor's first render
waits until the previous context's release has been consumed and its pair
parked, which successors on another VM already did.

Measured on Neo on the 7.2.10 line with diagnostic switches that apply the
first two rules:
 - four-client churn: 3 of 3 runs crashed within 10 to 120 s without
   them, 2 runs of 90 s passed with them
 - GLES: 15160 vs 15175 iterations, 5054 vs 5059 contexts per 90 s
 - Vulkan 4x MSAA render: 212643 vs 214050 iterations

tools/asahi/g17-owner-release-order-tests.py compiles the release table,
pair switch tracking and release readiness predicates against host stubs
and checks the guard placement in the rebind and publication paths.

Signed-off-by: DjDeveloperr <hey@djdev.me>
A pair installs the client alias of each kick ring into its VM when the
pair is installed (append_driver_mappings), and the VM keeps that alias
until it closes: a client that gets the pair back later reuses it. Under
identity pressure, reclaim_unused_render_spare() takes an installed pair
whose graph never started, for compute or for another render owner's
queue IDs, and drops it together with its rings. That frees the rings'
shared lower addresses while the VM that installed the pair, which may
outlive the closed queue (one DRM file with several queues), still
aliases the old pages there. A later ring placed at the same address and
handed to that VM cannot be aliased: prepare_client_alias() finds the
VM's mapping naming other pages and fails with EFAULT, and the client's
render is reported as a lost device. On the 7.2.10 line held GLES clients
hit this every few seconds while fresh clients came and went.

Keep the storage of a dropped ring and reuse it for later rings. An
address then keeps naming the same pages, so every alias a client keeps
stays valid; a reused ring is zeroed and keeps an existing alias. Storage
of a ring that ever wrote an entry is retained and never reused. A new
ring is counted only once its storage exists, reserving room to return it
then, so a dropped ring never allocates and a failed creation leaves no
reservation behind.

Signed-off-by: DjDeveloperr <hey@djdev.me>
A render packet that waits in publication for graph capacity already holds
its admission lease, and the entry of a pair under construction has no
installed pair. Admission, the capacity waits and the busy-pair polls each
have their own budget, so a publication can outlive the two-second
scheduler watchdog. timed_out() then counts the expiry towards the
timeout storm limit, and timeout_render() looks the pair up for an
unpublished packet, fails with EIO, and the handler loses the device for
every client. Eight such expiries within 30 seconds, which a burst of new
clients reaches, lose the device permanently even without the lookup.
On the 7.2.10 line a burst of new clients whose render free lists did not
fit the shared lower GPU VA window stopped the runtime two seconds after
the first wait began.

Each retry of such a build also populates the full free-list backing,
tens of MiB with ray-query scratch reservations, before the window is
searched, and frees it again after the refusal. With the 1 ms capacity
poll, sixteen concurrent builders reached several GiB of transient
kernel allocations.

 - A watchdog expiry for a packet that is not published returns NoHang
   without counting towards the storm limit and keeps the packet's place
   in the backlog: its publisher owns the bounded wait and the result.
 - If the timeout collector cannot be allocated, the expiry is retried at
   the next watchdog period instead of stopping the runtime.
 - A refused free-list placement is not attempted again for the same or a
   larger size until free-list backing is freed, apart from one probe per
   100 ms across all callers, so waiting builders allocate nothing.
 - While the window refuses free-list backing, idle retained compute pools
   are given back regardless of the warm cap.

The host resource tests check the placement gate and the watchdog guard
placement.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Every logical queue leases one of the 58 application contexts of the
TTBAT (5..62) when it is created. A closed queue keeps that lease until
its scheduler release has been consumed and the reclaim worker returns
it; retained compute queues and render pairs keep only storage, not the
context. After heavy device churn, contexts of queues closed moments
earlier can therefore still be leased while fewer than 58 queues are open,
and QUEUE_CREATE fails with ENOSPC from new_execution_context() although
the logical-queue count admitted the queue. Userspace creates its kernel
queue at the first submission and reports that as a lost device.

When the context allocation is refused and some leased contexts belong to
closed queues, kick the event worker and retry for a bounded time with
neither the device mutex nor the preparation lease held. Only open queues
then count against the limit. Nothing of the new queue is visible while it
waits, so a creator with a pending signal returns ERESTARTSYS. A refusal
that remains is counted and named as an exhausted execution context, next
to the existing logical-queue refusal, in the capacity diagnostics.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Dependency-free renders are published from the submit ioctl, which holds
the logical queue's mutex. try_render_early() only takes an admission
lease that is free at once, but render_with_lease() can then still sleep
for contention: render identities and capacity (up to a job timeout), a
borrowed or settling pair (up to a job timeout), work states (up to 1000
polls), recovery settling, and an uninterruptible wait on a closed
preparation gate. A client killed while its ioctl waited stalled for
that long, and on the 7.2.10 line, where the budgets were longer, for
about 30 s.

The submit ioctl no longer sleeps for contention. An ioctl publication
that would wait returns its admission lease and its publication claim
before anything became firmware-visible, and leaves the command to the
scheduler like a command with dependencies; later commands of the queue
then wait behind it in the backlog. The scheduler's worker keeps the
existing bounded waits. Uncontended ioctl publication is unchanged.

Compute publication also failed the packet with EBUSY when the context
had no free work state, although work states free as the context's
earlier commands retire. Wait for one under the packet's admission
deadline instead, as render publication does.

tools/asahi/g17-compute-deadline-tests.py runs the production compute
publication with a context that has no free work state and checks that it
waits until the admission deadline.

Signed-off-by: DjDeveloperr <hey@djdev.me>
…ir is borrowed

Event passes observe only render pairs that are installed in their entry.
A pair that preparation, growth or an observation has borrowed is skipped,
and nothing requested another pass when it came back: the completion of
work published on that pair waited for an unrelated later firmware event,
or for the one-millisecond rescan when no further event arrived.

Record a completion event that arrives for a borrowed pair and have the
borrower request an event pass when it returns the pair. Growth needs no
request of its own; its work item always queues one when it finishes.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Every compute command got a fresh page for its firmware start/end words:
a GEM object, its write-combined kernel mapping and a GPU mapping were set
up in the submit ioctl, and torn down again when the command's completion
was dropped. On Neo a lone dispatch spends ~95 us from its fence signal to
the next command's ioctl, against ~17 us for a render, which has no such
per-command allocation.

Keep up to eight retired pages per context, mapped in its VM, and hand them
out again with both words cleared. A page returns only when its completion
is dropped, which follows exact retirement or a kill witness, so the
firmware no longer writes it. Commands beyond the retained count still
allocate and free their page.

Signed-off-by: DjDeveloperr <hey@djdev.me>
@DjDeveloperr

Copy link
Copy Markdown
Contributor Author

Neo (A18 Pro) hardware qualification on 7.3.0-1-73fix-ARCH (asahi.ko 6a9882cca5f8):

held-run1: perf PASS kbad=0 4/4  {'compute': 18316, 'compute_rc': 0, 'render': 34625, 'render_rc': 0, 'render_s4': 34348, 'render_s4_rc': 0, 'gl': 12441, 'gl_rc': 0};fwcrash4 PASS kbad=0 4/4  ;mixed300 PASS kbad=0   ;held40 PASS kbad=0 40/40 fresh 12/12 ;
held-run2: perf PASS kbad=0 4/4  {'compute': 17955, 'compute_rc': 0, 'render': 34536, 'render_rc': 0, 'render_s4': 34101, 'render_s4_rc': 0, 'gl': 12159, 'gl_rc': 0};fwcrash4 PASS kbad=0 4/4  ;mixed300 PASS kbad=0   ;held40 PASS kbad=0 40/40 fresh 12/12 ;
perf PASS kbad=0 4/4  {'compute': 17764, 'compute_rc': 0, 'render': 34592, 'render_rc': 0, 'render_s4': 33959, 'render_s4_rc': 0, 'gl': 12224, 'gl_rc': 0}
fwcrash4 PASS kbad=0 4/4  
mixed300 PASS kbad=0   
held40 PASS kbad=0 40/40 fresh 12/12 
vkchurn PASS kbad=0 10/10  
compute20 PASS kbad=0 20/20  
render50 FAIL kbad=0 47/50  
qid48 FAIL kbad=0 47/48 fresh 0/12 
exit4 PASS kbad=0 4/4  
cycles100 PASS kbad=0   
expiry12 PASS kbad=0   
accounting PASS kbad=0 2/2  
perf PASS kbad=0 4/4  {'compute': 17718, 'compute_rc': 0, 'render': 34435, 'render_rc': 0, 'render_s4': 34107, 'render_s4_rc': 0, 'gl': 12321, 'gl_rc': 0}
EFAULT=0 VA=0 fw-crash=0 faults=0
STRESS-QUAL-DONE

Real-application mix (Chromium WebGL on GL and Vulkan, MSAA render, GL context churn and Vulkan compute device churn, 180 s):

chromium-gl.txt: RESULT ok frames=10803 bad=0 fps=60.0 err=0
chromium-vk.txt: RESULT ok frames=10802 bad=0 fps=60.0 err=0
gl-churn.txt: glstress pid=2635 renderer=Apple_A18_Pro_(G17P_B1) iters=39184 contexts=13062 bad=0 samples=1 layers=1 PASS
vk-compute.txt: vkstress pid=2636 mode=compute iters=381050 devices=7622 bad=0 samples=1 layers=1 PASS
vk-render.txt: vkstress pid=2634 mode=render iters=322925 devices=1 bad=0 samples=4 layers=2 PASS
desktop=up

No EFAULT, EBUSY, ETIMEDOUT, WARN, BUG, coprocessor crash or runtime stop in the stress boot's kernel log, and no compositor crash. render50 and qid48 are limited only by the named ENOSPC capacity ceilings. The accounting step reads the 7.2.10 asahi-g17 memory debugfs, which 7.3 doesn't have.

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.

1 participant