Skip to content

drm/asahi: g17: confine a GPU hang to the client that caused it - #928

Merged
RyanTheTide merged 9 commits into
aurora-silicon:integration/aurorafrom
jgoyvaerts:m4pro/g17-vm-hang-isolation
Nov 5, 2026
Merged

RyanTheTide merged 9 commits into
aurora-silicon:integration/aurorafrom
jgoyvaerts:m4pro/g17-vm-hang-isolation

Conversation

@jgoyvaerts

@jgoyvaerts jgoyvaerts commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

On the M4 Pro (25G83) every recovery slot key reads 0, so recovery attribution cannot check the firmware's blame. A fragment shader spinning on a lock (a deqp-vk memory-model case) then takes down unrelated clients:

  1. A restart drops only the guilty pair's started group, and its next queued group spins again.
  2. Each following recovery, about 83 ms later, blames whichever queue the spinner starved.
  3. That queue's oldest render fails with EIO and its VM is poisoned.
  4. On 2026-11-03 this killed Hyprland, quickshell and a terminal app in two cascades.

This series follows on from #803 and makes a recovery confine the damage to the client that hung:

  • name the client in failure reports: the failure warning names the process (comm and tgid) that created the VM. Without it, nothing in the log says which client a recovery hit.
  • blame the running VM, not a starved one, for a keyless timeout: when the slot keys can't judge a timeout, read the host's own pass records at the same halted instant. If the blamed render never started and exactly one VM has a render that started and did not finish, with no other VM's compute work, blame that VM.
  • keep another VM's recovery out of an innocent VM's status: a non-poisoning ECANCELED no longer reaches VM_STATUS, as the UAPI already documents. Honeykrisp reads any status error as a lost device.
  • do not count a failed VM's leftover hangs as a restart storm: a poisoned VM publishes nothing new, so the restarts that end its already-queued spinning groups are forgiven. Before this, 8 of them stopped the GPU for every client.
  • keep work a recovery dropped out of the VM status: same for a non-poisoning ENODATA (dropped, used-up replays, spared quarantine). The VM_STATUS comment now names ENODATA beside ECANCELED.
  • fail exhausted replays without poisoning their VM
  • keep a VM a recovery found guilty out of later spared work
  • blame a guilty VM's running render before a starved one and ... over a started one: a VM an earlier recovery found guilty that still has a running render takes the blame. That covers a bystander whose tiler started and whose fragment pass waits behind the spinner.

Testing (J616s, M4 Pro)

  • Run 4 (16 spinning groups), before the last commit: the desktop survived. glmark2, vkmark, Hyprland, quickshell and the terminal app all kept their pids, and the storm limit never fired. Of the 16 recoveries, 12 were firmware blames on the spinner and 3 were redirected by render_culprit. Every restart after the first was forgiven as leftover work. The one innocent EIO (a bystander whose render had started) is what the last commit fixes.
  • Unit tests: kunit 39 suites, 172 tests passed in a VM on the earlier base.
  • Live hang, before the last two commits: at 15:59 on 2026-11-03, a D3D12 test's fragment-shader hang on a test kernel was confined, and the desktop survived.
  • Daily use: the full series has been on our daily kernel since 2026-11-03 20:13, on aurora-wip 7399ba907087. No GPU recovery has happened on it since.
  • This branch: a clean cherry-pick onto integration/aurora 7d581f615cd0, plus three comment fixes from review. drivers/gpu/drm/asahi builds with no new warnings, and checkpatch reports only lines over 100 columns.

Known limits

  • A started bystander with no earlier guilty VM. The firmware blames a bystander's render that had started while the real culprit's render runs, and no earlier recovery found the culprit guilty. Nothing moves the blame then. This was seen once, at 15:14 on 2026-11-03: 7 recoveries ended at the storm limit.
  • A second VM that hangs while a guilty VM still has started renders. It is blamed only once those renders are gone, and those restarts are forgiven. The evidence available cannot tell it from a starved bystander.
  • A restart that finds nobody guilty (no blamed queue and no guilty entries, or owners it cannot resolve). All interrupted work fails with ECANCELED alone, so only the storm limit stops a culprit that keeps hanging.
  • A blamed compute queue keeps the firmware's blame: only render pairs have the pass records the redirect reads.

AxxzyWasTaken's aurora-silicon/mesa#157 ("asahi: survive a failed VM after GPU recovery") is the userspace side of the same situation.

🤖 Generated with Claude Code

"GPU work of a client failed" did not say which client: on a desktop with a
browser, a game under Wine and a compositor, 31 such lines in one boot
could not be attributed. Record the creating process (tgid and comm) with
the VM status and print it.

The message keeps its prefix, which tooling matches.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
…s timeout

On the M4 Pro (25G83) every recovery slot key reads 0, so recovery
attribution cannot check the firmware's blame and always keeps it. A
fragment shader spinning on a lock then takes down unrelated clients: a
restart drops only the guilty pair's started group, its next queued group
spins again, and each following recovery 83 ms later blames whichever
queue the spinner starved. The driver fails that queue's oldest render
with EIO, which poisons its VM. On 11-03 that killed Hyprland, quickshell
and T3 Code in two cascades of deqp-vk hangs. In five of the six
innocent blames the blamed render had never started (tiler start and
fragment end unwritten).

When slot identity cannot judge a timeout (not a page fault), read the
host's own pass records at the same halted instant: if the blamed pair's
oldest published render has not started, and exactly one VM has a
published render that started and did not finish, and no other VM holds
compute work, blame that VM's queues instead. Groups an earlier recovery
listed or blamed are skipped; the restart dropped them, so their records
stay half-written. In every other case the firmware's blame stands, as
before.

A blamed compute queue keeps the firmware's blame: only render pairs
have the pass records this reads.

This does not stop the guilty pair's queued groups from running after
the restart, so a client with many of them can still reach the restart
storm limit.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
…atus

A command a GPU restart interrupted fails with ECANCELED. Unless a
recovery found its VM guilty, it does not poison the VM, but it was still
passed to VmStatus::report_failure, which stores it in the VM status
that GET_PARAMS VM_STATUS and the status mirror return. Honeykrisp's
check_status treats any nonzero status as a lost device, so every Vulkan
client with work in flight lost its device in another client's
recovery, while GL clients, which never read the status that way,
survived. The UAPI documents the opposite: a job cancelled because
another VM's recovery discarded it is not a failure of this VM, and its
error stays 0.

Report a non-poisoning ECANCELED to nobody, as was already done for work
cancelled by its owner. The fence still carries ECANCELED, the restart
is still logged ("interrupted by a GPU recovery after it started"), and
a guilty VM is still poisoned with it. Under Vulkan such a submission's
output is undefined, as the UAPI says, instead of the device being lost.

A restart that finds nobody guilty (no blamed queue and no guilty table
entries, or owners it cannot resolve) now fails all interrupted work this
way, the culprit's too, so a culprit that keeps hanging is stopped only
by the restart storm limit.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
…t storm

A firmware restart drops the groups that had started, but a hung client's
other published groups run as usual afterwards. A client that queued the
same spinning draw many times (deqp-vk on 11-03 had 17 renders retained
on one pair) therefore hangs the GPU once per group, 83 ms apart. Once
the blame lands on that client, its VM is poisoned and it can publish
nothing new, so the cascade ends with its queued work. Yet the eighth
restart within 30 s reaches the storm limit and stops the GPU for every
client.

When every VM a recovery finds guilty had already failed before it, and
the recovery table matched one of their commands by its exact stamp (so
the restart dropped it), take that restart back from the storm count.
At most RENDER_DEPTH restarts are forgiven per window, so an unknown
firmware loop still reaches the limit. A first failure always counts.

The host cannot stop the queued groups themselves: 25G83 has no recovery
target word, and a ContextKill is only sent for closed contexts. Releasing
a killed live context's commands would depend on the kill acknowledgement
waiting for a running spinner, which is unverified.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
When a GPU restart drops an innocent pair's groups and they cannot run
again in order, fail_dropped fails them with ENODATA without poisoning
the VM. The failure was still passed to VmStatus::report_failure, which
stores it in the status that GET_PARAMS VM_STATUS and the status mirror
return, and Honeykrisp's check_status treats any nonzero status as a
lost device. In the 16-group spin repro on m4.119, vkmark lost its
device this way twice ("failing 2 groups ... ENODATA ... the client
sees a lost device") although no recovery blamed it.

Treat a non-poisoning ENODATA like a non-poisoning ECANCELED: the fence
carries the error, the VM status stays 0. Every other ENODATA source
(compute replay and spared quarantine) is likewise work a recovery
dropped, and a poisoning ENODATA is still recorded. The VM_STATUS
UAPI comment now names ENODATA beside ECANCELED.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
A pair whose groups a restart drops untouched has them republished, up
to MAX_REPLAYS times. In a cascade of restarts another client causes,
the same innocent groups are dropped on every restart, and the fourth
drop failed them with EIO recorded on their VM: T3 Code lost its device
in the 16-group spin repro on m4.119 ("12 groups retired without
execution; replay limit reached, failing them").

replay::decide() only replays groups that are innocent, not blamed and
never ran, so none of them can have caused the hang. Fail them like
fail_dropped does: spared, with ENODATA, which stays out of the VM
status. The replay limit still bounds how often they are republished.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
… work

Each recovery classifies every VM it does not find guilty as spared. A
hung client's VM that an earlier recovery found guilty therefore turned
spared as soon as one later recovery blamed someone else. The next
recovery that blamed it again then found its oldest work spared, took
the blame as collateral to that recovery, found no guilty VM, and so
did not treat the restart as leftover work of a failed VM: it counted
toward the restart storm limit. In the 16-group spin repro on m4.119,
gens 9 and 10 blamed glmark2 and quickshell, and the four correct
blames on the spinning client after them all counted; the next restart
reached the limit and stopped the GPU for everyone.

Remember on the VM status that a recovery found it guilty, and keep
such a VM's work guilty in every later recovery. A VM that failed only
for its own client is still spared.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
render_culprit moves a keyless timeout's blame off a render that never
started only when exactly one VM has a render that started and did not
finish, and no other VM holds compute work. Under load that rarely
holds: bystanders' tilers start and their fragments then wait behind
the spinning one. In the 16-group spin repro on m4.119, gens 9 and 10
blamed glmark2 and quickshell, whose blamed renders had neither pass
written, and both lost their device.

If a VM an earlier recovery found guilty still has such a render, blame
it first: its groups queued before that recovery keep hanging the GPU
one per restart, and it has already failed. Only otherwise does the
one-running-VM rule apply.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
render_culprit moved a keyless timeout's blame only when the blamed
pair's oldest render had not started. A bystander's render whose tiler
started and whose fragment pass then waits behind the spinning one is
just as starved. On m4.120, in the 16-group spin repro, 15 of the 16
recoveries landed on the spinning client, 3 of them through
render_culprit. In the 16th, the firmware blamed T3 Code, whose render
had its tiler start written and no fragment end, while the spinner's
last group ran. T3 Code lost its device.

When a VM an earlier recovery found guilty still has a render that
started and did not finish, blame it whether or not the blamed render
had started: that VM has already failed, and its queued groups end with
the restarts they cause. The one-running-VM rule, which can blame a VM
that has not failed, still needs a blamed render that never started.

Known limit: if a second VM hangs while a guilty VM still has started
renders, each restart blames the guilty VM until those renders are gone,
and those restarts do not count toward the restart storm limit. The
evidence here cannot tell that second hang from a starved bystander,
which is the case this change is for.

Signed-off-by: Jonathan Goyvaerts <jonathan.goyvaerts@gmail.com>
Assisted-by: LLM
@joshuaswarren

Copy link
Copy Markdown
Contributor

[Joshua's agent] Thanks. The two cascades on 11-03 make a clear case for this. We will run the series on our 16-inch M1 Max for regressions and post the results here.

@joshuaswarren

Copy link
Copy Markdown
Contributor

[Joshua's agent] Boot and probe test of this PR on a 16-inch M1 Max MacBook Pro (G13C GPU). Head 6295b02 on integration/aurora 7d581f6 (Linux 7.3.0).

Scope: this tests build, module load, the g13 probe and the UAPI header. It says nothing about g17 behaviour. The PR changes only g17 code and comments in the UAPI header, and an M1 Max does not run g17, so "build" here means the g17 files compile into the module.

Results (one-shot kernel, boot at 13:42Z):

  • UAPI: include/uapi/drm/asahi_drm.h differs from base in one hunk (3 lines added, 2 removed, all comments: the ENODATA wording). After stripping comments with gcc -fpreprocessed -dD -E -P, both files are byte-identical (10262 bytes).
  • Build: the PR builds as a loadable module, aurora.ko (package sha256 6d0b697c...; the module in it is the one the kernel loaded, sha256 89743045..., and it contains the g17 files). I did not check compiler warnings: the saved log has only the link and package steps. Its packaging step ends with depmod errors about a staging directory, and the package was still written.
  • Boot: the kernel boots with 0 failed units. smoke.sh gives 6 PASS (boot, Wi-Fi, input presence, display/backlight, ANE, battery) and 3 FAIL: input-errors (SPI keyboard "waiting for get report failed: -110" at 3.4 s), audio (aplay -l as a normal user lists no sound card; as root it lists the J316 card with 2 playback devices) and gpu (no Vulkan device, see below). smoke-extra gives gpu-compute FAIL and gpu-vulkan FAIL (llvmpipe only), gpu-dmesg PASS and audio-card PASS. The results are the same at 64 s and 120 s of uptime. I did not compare them with base.
  • GPU probe: dmesg shows "[drm] Initialized aurora 1.0.0 for 406400000.gpu on minor 2", G13C, 4 clusters, 8 cores per cluster. No timeout, fault, fence or oops lines.
  • Ioctl test: I wrote a small C program that opens the render node and runs GET_PARAMS (gen 13, variant 67, chip 0x6001), GET_TIME, VM_CREATE, GEM_CREATE, GEM_MMAP_OFFSET, a read/write of 16384 words through the mapping, GEM_CLOSE and VM_DESTROY. It also sends a bad param group and checks that the kernel refuses it. All 12 steps pass.

Not run: glmark2, vkmark and the MLX decode on the Apple GPU. On the 7.2.15 kernel from #362, which registers the DRM driver as "asahi", Honeykrisp works on this laptop. On this kernel, which registers it as "aurora" (drivers/gpu/drm/asahi/driver.rs:92; Kconfig:29 keeps "asahi" only as a module alias), Vulkan lists only llvmpipe. The PR does not touch that line. I believe the installed Mesa (26.2.4) matches on the driver name. I checked only that libvulkan_asahi.so contains the string "asahi" and not "aurora"; I did not read the Mesa source.

Limits:

  • No base control run. The base build was still running when my slot ended. The PR kernel logged these lines, and I cannot say whether base logs them too: SPI keyboard report timeouts, "tps6598x cable revalidation failed", and "thunderbolt-apple-acio: PCIe-C is not initialized by m1n1 or the kernel". The PR changes none of the code behind those lines, but I have not measured base. No Hyprland session was running either.
  • An eGPU was connected. It did not enumerate on this kernel (no 0001 PCIe port appeared, and the ACIO line above). It did enumerate on the G17P Mesa: the USC window base container needs a terminating CODE block (fragment-end page fault, found on M4 Pro) #362 kernel in the same session. I have no base control for this. The kernel command line carries lab eGPU options this kernel does not know ("PCI: Unknown option noiovbars").
  • Boot order was fixed: a different kernel booted first, then this one. I did not randomise it.

RyanTheTide added a commit that referenced this pull request Nov 5, 2026
…xture

Merge train: G17 hang confinement (#928) plus its host-test fixture
@RyanTheTide
RyanTheTide merged commit 6295b02 into aurora-silicon:integration/aurora Nov 5, 2026
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.

3 participants