Skip to content

drm/asahi: improve G17 submission, queue admission and completion - #423

Merged
DjDeveloperr merged 65 commits into
integration/aurorafrom
perf/g17-submit-completion-20261024
Oct 26, 2026
Merged

DjDeveloperr merged 65 commits into
integration/aurorafrom
perf/g17-submit-completion-20261024

Conversation

@DjDeveloperr

@DjDeveloperr DjDeveloperr commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

G17 submission spends substantial time waiting for host scheduling, holding the device mutex, rebuilding firmware records and walking growing fence histories. This series moves steady-state work onto the queue that owns it and retains the existing recovery and fallback paths.

The series covers the finalized GPU work through 30f85560fd28, rebased onto integration/aurora at d3cf19be28d5 (includes the fabric DVFS fix, #440). It remains a draft while the remaining benchmark gaps are addressed and the final kernel/Mesa pair completes sustained stress and full applicable Vulkan CTS qualification.

Changes

  • Publish ready compute commands from the submit ioctl at any queue depth; keep bound compute publication and steady-state render publication off the device mutex.
  • Replace admission retry sleeps with resource-specific wakeups and scheduler preparation dependencies. Account render queue depth through scheduler credits.
  • Allocate compute QIDs according to open logical queues, retain backing under channel pressure, and preserve room for later render queues. Firmware-owned QIDs remain excluded.
  • Keep compute descriptors, command records and queue state in retained firmware-cached mappings; avoid redundant clearing, configuration messages and completion scans. Every compute publication still rings its own doorbell.
  • Express eligible same-VM compute dependencies and overlapping render attachments as firmware waits, preserving host waits for unsupported or cross-VM dependencies.
  • Add bounded cumulative completion scopes, native input collection, error/deadline propagation and the inline syncobj accessor. The private cumulative-output type is advertised by feature bit 17 and is used only after userspace detects support.
  • Add opt-in command, doorbell and host-message tracepoints. Register sampling occurs only when the tracepoint is enabled.
  • Stop lazy polling of an all-of DMA fence array at its first pending member; include regression coverage for completion order and callback installation.

Recovery under overload (added 2026-10-25)

Qualification at six concurrent CTS workers found that overload could take down the whole GPU. Six concurrent dEQP-VK.ray_query.stress clients reliably reproduce it. Three commits fix it:

  • Restart handshake budget. The firmware finishes a restart only after the interrupted work drains. Under this load that took longer than the 500 ms host budget, so the device was declared lost. Each phase now waits up to 10 s, and a timeout logs its phase and firmware state.
  • Interrupted commands fail visibly. Every command the firmware recovery table lists as in flight is now marked interrupted. It never reports success unless it provably never started, in which case it is replayed. Before this, three innocent clients completed without error but with wrong results.
  • Progress-aware watchdog storm limit. Eight job timeouts in 30 s used to stop the device even while other work kept retiring. The window now restarts whenever work retires, and a GPU that retires nothing still stops.

Result on the same load: the GPU stays online, the desktop survives and no client gets wrong data. The overloaded clients see a lost device, as they would on macOS, whose firmware timeouts are the same.

On this head plus the fabric DVFS fix (#440) and Mesa #62:

  • CTS: 1,145,669 non-RT cases at 6 workers gave 608,807 Pass, 0 Fail, 0 Crash, 4 benign QualityWarnings. RT groups are pending.
  • Stress on the recovery-budget build: 100k cross-queue order/data iterations, 3×300 s mixed pressure, 300 s churn and concurrent RT/hop loops all passed.

Userspace and compatibility

Companion: Mesa #62, including the cumulative queue-signal change originally qualified as e3039f098426. Existing userspace keeps its current output path, and the companion retains its fallback on kernels without the feature. Cumulative completion does not add execution dependencies between otherwise independent GPU submissions.

Integration notes

The target branch has gained additional G17 variants and recovery fixes since the original qualification:

  • The local all-groups render-replay commit f602f7151dd8 is omitted because the target already supplies that behavior through 645b4e55cf16 and subsequent recovery fixes. Guilty-work guards, spared quarantine and the render scan before timeout attribution are retained.
  • Variant pointer layouts, operand sizes, initialization acknowledgments, optional secondary coprocessors, feature reporting and idle-hold behavior are preserved in the direct paths.
  • Compute and render use one atomic watchdog sequence. It records reservation order; concurrent engines may expose commands in another order. Queue serialization and visibility remain independently guarded, and the watchdog uses the sequence for bounded cross-VM timeout grace.
  • QID capacity subtracts the firmware-owned prefix and caps render headroom at the variant's logical-queue limit. Added tests exercise both zero and nonzero prefixes.

This PR excludes the mailbox hard-IRQ experiment, timestamp experiment and unfinished Mesa launch work.

Native-wait record bounds

The dynamic QID budget also raises the native-wait bound from 10 to 128. Compute wait records begin at offset 0x900 in a 0x1000-byte slot, with 0x60 bytes per wait. A 19th wait crosses into the next slot. The original 10-wait bound stayed within that allocation; the larger bound in this series exposes the problem.

The record-bounds commit sizes the cached slots to 0x4000 and validates each write against its own slot before forming the allocation offset. Announcement slots remain 0x1000. This adds 384 KiB per physical compute queue and does not reduce queue concurrency or the input bound.

The production offset helper and its test bodies pass on the host for all 128 waits in all 32 slots, including invalid-window and overflow cases; an old-size negative control fails. First-command native waits retain the existing EAGAIN fallback before record selection, so they do not use the inline record region. Extracted guard and window checks cover that fallback and the inline boundary as well. Real Vulkan tests now pass with 19 and 20 simultaneous native inputs, three rounds each: the native-input counter advances by exactly 19/20, every producer result and the consumer sum match CPU checks, adjacent result allocations remain intact, and no GPU fault is reported. These tests use a test-only 21-queue Mesa override; production queue limits are unchanged. Host coverage spans all 128 supported waits; hardware boundary coverage is 19/20, not an arbitrary-128-input claim.

Follow-up CPU efficiency changes

The follow-up completion lookup indexes requested QID/kick identities before reading record payloads. It keeps the complete scan and retirement fallbacks, including missing-stamp and recovery handling. Five source test cases and a 10,000-record differential check against the original collector preserve exact matching, collision handling and error observations. In matched workload profiles, the compute scanner's self samples fell from 4.87% to 2.24% at depth 64 and from 3.16% to 1.93% in the queue-hop workload.

The validation cache retains page ranges that the VM already checked, including their permissions and mapping epoch. It keeps 128 persistent intervals while the transient parser remains bounded at 48 requests. This avoids the measured depth-64 working-set thrash without changing mapping lifetime or GPU concurrency. Repeated normal runs reduced compute depth-64 submit API elapsed time from 10.06 to 7.46 us and render depth-32 submit API elapsed time from 12.29 to 8.16 us. These benchmark metrics use a monotonic wall clock around the API call, so they include scheduling and lock waits; they are not per-thread CPU-clock measurements. Page/permission/epoch/overflow tests and a rotating 64-buffer test pass.

The scanner profile establishes a CPU-cost reduction; the submission benchmark establishes lower API elapsed time. The combined candidate measured 19.44 us at compute depth 64, 41.48 us at render depth 32, and 42.12 us per queue hop. They do not establish parity with the recorded macOS throughput targets.

The next frontier change checks the signalled flag of driver-private completion fences before taking a status lock. These fences are explicitly signalled and have no polling callback. Pending observations can only retain a frontier member conservatively; terminal statuses still use the normal locked API, and external inputs keep their generic fence handling. Seven depth-64 repetitions reduced median submit API elapsed time from 7.52 to 5.79 us. Matched profiles reduced frontier-scan samples from 1.83% to 0.25% of the machine. Throughput stayed near 19.5 us per command, so this is a CPU-efficiency improvement rather than a throughput-parity claim.

Validation

Original finalized stack on Neo:

  • All 19 benchmark rows and the recorded mild stress gate completed, with the desktop alive and no GPU fault reported. A later harness audit found that its 35-second pressure phase forced an older Mesa ICD; the direct CTS/order/readback/churn phases used the selected driver. Corrected explicit-ICD pressure testing is required for final qualification.
  • Published software-fence ordering, mixed compute/render and empty-submit waits, binary/timeline synchronization, deadline forwarding, a 256-completion backlog and injected errors passed.
  • Same-boot compute submit API elapsed time fell from approximately 14 to 10 microseconds with cumulative scopes; GPU throughput remained near 20 microseconds per submission. The elapsed-time reduction varies by workload.
  • Historical end-to-end results include compute depth 64 at 19.71 us, 16-process compute at 25.68 us, eight-queue render at 58.77 us and queue-hop latency at 43.74 us. These are measurements of the finalized kernel/Mesa stack, not a kernel-only comparison or a claim of parity on every workload.

Rebased candidate:

  • External Asahi module build against the retained 7.3.0-1-perf-ARCH kernel (core aa79324715fb): passed; latest module build ID d1b39857cb8e51b1f5249d3f1c11af3fa3205f43, source 5b25f951ab88. This is not a full boot of the target integration kernel.
  • git diff --check: passed.
  • Six QID accounting tests, including the two new firmware-prefix cases, and two record-window tests: passed using the actual source methods/helper and test bodies with host shims. The old record-slot-size negative control fails. These are not kernel KUnit runs.
  • Combined source 3049029c1ebd with Mesa 051a: all 19 benchmark rows and the existing stress suite passed. A separate 35-second, 18-child pressure run explicitly used the selected ICD. Published software-fence ordering, deadline forwarding, 256-completion backlog and error propagation passed; 20 simultaneous native inputs passed three rounds of result and neighboring-allocation checks. No GPU faults were reported. These bounded checks do not replace the sustained stress and full applicable Vulkan CTS still required, alongside the remaining performance targets.
  • Source 5b25f951ab88 with Mesa 051a: all 19 benchmark rows, corrected candidate-bound short stress, software-fence ordering/deadline/backlog/injected-error tests and 20-source native fan-in passed. The small compute/synchronization CTS gate recorded 116 Pass and 18 Skip; this is not the full qualification suite. No new GPU faults were reported.
  • T6040 hardware validation and full Vulkan mustpass: not run for this series.

Complete benchmark scope and required order

The identified full standard and RT run used source 5b25f951ab88, module
d1b39857cb8e51b1f5249d3f1c11af3fa3205f43, retained core aa79324715fb,
and Mesa 562393785267. It produced all 209 recorded configurations, with a live
desktop and zero new GPU faults. Against the exact recorded macOS run, 55 of
207 comparable primary medians strictly win, 143 are slower and 9 tie. The two
macOS pipeline skips remain explicitly noncomparable. The 19-row development
matrix and nine representative rows do not establish this full gate. These
are measured medians, not statistical significance or sustained qualification.

All CTS work, including preparation and small slices, is stopped until Linux
strictly exceeds macOS in every comparable benchmark. Then the final pair must
pass proper sustained stress, full applicable Vulkan CTS, and only then merge.
Both companion PRs remain draft and unmerged. Earlier CTS results below are
historical; they do not satisfy the final gate. Experimental scheduler and
query changes are not included in these published heads.

The companion Mesa PR now includes clean commit d6d814afdb8, fixing missing relocation helpers and inactive/empty TLAS handling. Its exact rebuilt artifact passes all 17 TLAS phases / 136 ray-transform comparisons, all 68 BLAS/AABB phases / 544 ray checks, and 16 concurrent RT clients (680 phases / 5,440 comparisons), plus query/data/order and bounded compute/render pressure gates on this qualified kernel module. These fix correctness and do not establish an RT throughput gain. The complete 209-row record above belongs to the preceding Mesa 562 artifact; no full performance record is claimed for the new companion head.

The companion now also includes 9bbce5b1798 and clean head 7c68adfc257, enabling existing uniform DMA for ordinary internal compute pipelines and using existing linear PBE writes for eligible G17P buffer fills of at least 64 MiB. Exact rebuilt library SHA256 0a2c688bbc98650e6c7771602599abdb1aea8bff91ddfa35fa55a3bc574409ee, build ID 53a9fb9740865d0f8426fc5f40d38a2b67f237d0, passed 24 query/error cases, 49 core rows, 28 fill/copy rows, 15 GPU boundary cases, eight concurrent fill clients (120 cases / 2,013,511,680 checked words), order/data checks and bounded mixed pressure on this unchanged kernel. Large-fill times improve about 2.9–4.0%; representative core results remain essentially flat. Linux still trails the recorded macOS fill results by about 28–29%. These bounded checks and partial gains do not satisfy the all-benchmark, sustained-stress or CTS gates. All generated precompiled helper C is unchanged from the preceding companion head; the earlier AS fixtures remain prior-head evidence.

Completion scan reads only new records (2c0abe2)

While any command was in flight, the compute completion scan read all 2048 ring slots on every pass (about 3 µs before any fence was signalled), and its cursor never advanced while a queue stayed busy. It now folds only the records appended since the last pass, with a one-time full read as a fallback for a command whose stamp already covers it.

Same boot on Neo, patched cpufreq (#443), app on cpu5, against the previous scan (µs):

row previous this macOS
compute d2 43.09 33.70 40.93
compute d64 14.14 13.94 13.99
idle.wake 2 ms 119.2 117.3 153
concurrent interleave 32.95 31.87 31.98
compute.heavy alu queues d8 1234.6 1214.1 1262.6

Gate: order/data 10k, 35 s pressure and mixed churn passed, with no new faults. Targeted CTS (synchronization2 basic + timeline_semaphore, api.command_buffers, compute.pipeline.basic) under a 6 GB cap gave 3,023 pass, 84 not supported and 0 failures.

Render submit path (39bdd5b..66ef051, 8 commits)

Render ioctl entry → doorbell goes from 13.67 to 11.23 µs (same-boot phase timers); compute goes from 8.90 to 8.36. The changes:

  • release pruned frontier entries after publication;
  • publish both render QoS owners in one reads-before-stores pass;
  • drop the copy into the unused client register page;
  • read the render work rings once per direct publication;
  • prefetch the tiling descriptor and queue configuration records;
  • store register arrays straight into the descriptor;
  • scan only render pool slots for free-list leases.

Same boot on Neo, three rounds alternating per row (µs per submit; app pinned to cpu5 unless noted):

row before after macOS
render d1 115.4 104.9 239
render d2 47.4 46.9 48.3
render procs d2 (unpinned) 58.9 55.5 58.0
buf2img d1 / d4 1572 / 1471 1553 / 1366 1572 / 1429
compute d1 / d2 / d64 104.0 / 33.8 / 14.3 103.7 / 33.1 / 13.9 111 / 40.9 / 14.0

Render d32, render queues and hop are unchanged; they are bounded by firmware completion work.

Gate passed. Targeted CTS (synchronization2 basic + timeline_semaphore, api.command_buffers, renderpass1/2 suballocation, dynamic_rendering primary basic/random): 5368 cases, 4687 pass, 681 not supported, 0 fail.

Drop redundant render queue configuration records (1aec5be)

A render published onto a drained pair forced a queue configuration record onto both item rings, even when the queue already held exactly the value being seeded. The firmware processes each record before starting the kick, at ~1.5 µs each. The record is now omitted when it would only reload the held completion. Recovery, replay and graph rebuilds still seed.

Same boot, alternated per row, 3 rounds (µs per submit):

row before after macOS
render queues d2 54.70 51.88 53.7
render queues d4 45.25 42.15 39.5
render queues d8 41.65 38.79 35.7
render procs d2 54.68 51.84 58.0
render procs d16 40.84 37.63 38.6

Gate passed. Targeted CTS: 3027 pass, 84 not supported, 0 fail.

One CPU mapping per timestamp buffer (d0df244)

Every timestamped command mapped its destination buffer into the kernel per timestamp (taking the reservation lock), then queued a cleanup work item at completion. A timestamp buffer is now mapped once, when it is bound; its last-reference release is deferred once per buffer.

Same-boot phase timers, render d1 on cpu5:

step before after
destination resolve, render 410 ns 132 ns
destination resolve, compute 367 ns 152 ns
destination resolve, occlusion 337 ns 141 ns
completion teardown 578 ns 153 ns

End-to-end rows are unchanged within noise. Gate passed. Targeted CTS: 23,638 cases including all 18,270 query_pool.*, 22,847 pass, 791 not supported, 0 fail.

Withhold compute doorbells behind unretired commands (3ff8c49, b1f5dc6, d38cce2)

A compute doorbell makes the firmware read its priority class's whole work ring. A doorbell it handles while finishing a command delays that command's completion stamp from about 12 µs to 24 µs, and with it every waiter. A publication with two or more unretired commands of its queue ahead cannot start soon. Its doorbell is now withheld until at most one command precedes it, and is dropped if a doorbell of the same class rung meanwhile already announced its slot. Any departure from ordinary retirement rings it at once: timeout, kill, quarantine, replay, recovery or exit.

Cross-queue hop (µs per hop):

row before after macOS
sync.signal queue, timeline 30.13 22.70 19.5
sync.signal queue, binary 29.14 23.02 19.1

Gate passed. 10k order/data iterations passed. Targeted CTS: 3555 pass, 0 fail.

Multi-client stress (added 2026-10-25)

An external five-lane stress run on this branch reported more GPU timeouts than on the base. A five-lane harness reproduces it on Neo: global-priority preemption CTS, process churn, render fill and geometry, and a client looping 1.5 s compute dispatches.

  • On this branch before the fix, the firmware collapsed within 3 minutes: 31 host timeouts, 62 restarts, then a firmware crash and device loss, 40 ENODATA and 38 CTS crashes.
  • The base driver's firmware crashed at the start of the run.

Cause:

  1. Under load, the 2 s scheduler timeout measured how the GPU was shared, not whether a job progressed. Beside four other clients a 1.5 s dispatch took up to 5 s, and low-priority work waited behind higher-priority work for seconds. Meanwhile the GPU kept retiring other commands at least every 1.2 s. Those jobs failed although nothing had hung.
  2. The failed client closed its queues, and 250 ms later the driver killed the closed context. A kill while other contexts' commands are running makes the firmware restart the GPU, 2–60 ms later, failing all in-flight work; repeats crash it. The same kill on an idle GPU is handled cleanly (four isolated rounds).

Enabling firmware time-slicing (context-switch timer on compute, or on all engines) was tested and made things worse; it stays off.

Fix:

  • 2d82861: build without warnings. Kernels with CONFIG_WERROR failed: the tracepoint entry points had no C prototypes, and the series left unused Rust items. Reported by RyanTheTide.
  • 9a0ed80: liveness watchdog. A published job's deadline is renewed while any command on the GPU retired in the last 5 s, up to 30 s in all. A GPU that retires nothing for 5 s still fails its jobs.
  • afbfbc0: kill gating. A closed context is killed only while no other context has work in flight, or once the GPU has retired nothing for 5 s. Until then its queues stay quarantined and its commands retire normally, as ray-traversal commands already did.

Same harness, 20.5 minutes active:

before (d0df244) after
minutes until collapse 2.8 none
firmware restarts / crashes 62 / 1 0 / 0
host timeouts 31 0
CTS global_priority crashes 38 0 (79 iterations, 1896 passes)
commands over 2 s that completed — 10 (longest 4.67 s)

Ten gpubench rows of 444 missed their 300 ms measurement windows while sharing the GPU with the 1.5 s dispatches. No client lost its device.

Gate passed on the fixed build: cross-queue data, 29,476 mixed stress iterations with 0 bad, pressure cycle; no driver messages in the kernel log. Targeted CTS (synchronization2 basic + timeline_semaphore, api.command_buffers, renderpass1/2 suballocation and subpass dependencies, dynamic_rendering primary basic/random): 5368 cases, 4687 pass, 681 not supported, 0 fail, 0 crash. Each new commit builds with -Werror.

Full gpubench record on afbfbc0 with Mesa #62, built from this exact head: no driver errors, and gate-eligible provenance. A four-boot A/B on the same kernel core compared d0df244, the doorbell series alone, afbfbc0, and d0df244 again:

row (µs) d0df244 doorbell series afbfbc0 d0df244
compute.light queues d2 35.0 33.8 34.9 34.5
compute.light procs d2 35.5 35.5 35.7 34.5
idle.wake (2 ms gap) 118.9 120.0 121.3 120.0
cross-queue hop, timeline 30.0 23.3 23.4 30.5

The stress fix costs nothing measurable. The remaining rows are unchanged.

Recovery: dropped render groups (added 2026-10-25)

Forced GPU recoveries under the same stress, with an oracle lane checking data, found two pre-existing render bugs:

  1. Stranded groups. After a restart the firmware retires, without running them, every group it had started on a pair: the listed ones, plus unlisted groups whose tiler had run while the fragment was pending. Groups it had not started run normally. The host's in-order replay waited at the first group that ran, so the pair sat until the render watchdog fired (2–30 s) and the owner got a sticky ETIMEDOUT. Three of 11 forced recoveries hit innocent clients this way.
  2. Silent corruption. If a compute command waited on a dropped group, that firmware wait ended when the group was retired unexecuted. The consumer read stale data, and replay ran the render afterwards. The oracle caught it as a render mismatch equal to the previous iteration's colour.

31279ab: when the firmware has retired the whole set but a dropped group can't run again in order, the set fails at once with ENODATA, visible to its owner without poisoning the VM. That covers a later group that already ran, a group that had started, or a group a compute command waited on. Pure-render sets still replay, so innocent render clients survive.

7012a59: after recovery, any still-withheld compute doorbell is rung before replays publish.

With forced recoveries (14):

  • render watchdog fires after a recovery: 10 → 0;
  • 18 dropped sets failed within 2.7–17.5 ms;
  • 290 outstanding commands at recoveries all resolved within 16 ms, none left unsignalled;
  • oracle silent corruption: 1 → 0.

Production stress (600 s, no fault injection) was all-zero errors, with 45 CTS iterations clean and the oracle 38/38. Gate PASS; targeted CTS (query_pool included) 23,638 cases, 0 fail. A four-boot A/B against afbfbc0 is flat on every compute, render, hop and submit row.

Cumulative signals move to feature bit 29 (8e257af)

Bit 17 is also used by the M5 userspace line, whose features take bits 17–28. Cumulative signals move to bit 29 in lockstep with Mesa #62. On this kernel a kprobe counted 0 cumulative outputs with the old Mesa (it falls back to per-point signals) and 74,260 with the new one. Gate PASS, targeted CTS 4687 pass / 681 not supported / 0 fail, and an rb5-vs-rb6 A/B flat on every row.

@RyanTheTide

Copy link
Copy Markdown
Contributor

Heads up while you iterate. This doesn't build with the universal recipe (Fedora asahi base + asahi.config + olddefconfig, LLVM=1, so -Werror is on) on top of 73w (integration/aurora 420a1c8 + #394).

drivers/gpu/drm/asahi/g17/trace.c fails with 8 errors, "no previous prototype for function" [-Werror,-Wmissing-prototypes], for:

  • asahi_g17_trace_publish_enabled, asahi_g17_trace_publish, asahi_g17_trace_complete, asahi_g17_trace_bind;
  • asahi_g17_trace_doorbell_enabled, asahi_g17_trace_doorbell, asahi_g17_trace_message, asahi_g17_trace_event.

They're only called from Rust, and trace.h is just the TRACE_EVENT header, so nothing in C declares them. An external module build doesn't turn -Werror on, so it passes there.

A small trace_api.h with the 8 prototypes, included by trace.c (and usable for bindgen too), would fix it.

I'm carrying on with the qualification (one-shot boot on the Neo, full applicable Vulkan CTS with #62, stress, benchmarks) on a local commit that only adds those prototypes to trace.c. It isn't pushed to your branch. Full results will follow in one comment when they're done.

A compute command on a bound queue went through every admission step on
each submission. ensure_compute took the device mutex for a replay and
witness pass, shared_compute took a preparation lease and the mutex
again, and build_compute did both once more to find its queue already
bound. compute_packet then took a third lease and the mutex a fourth
time, replaying compute again before publishing. Compute also never
published from its submit ioctl, so even a command with nothing to wait
for went through the scheduler worker first. Its completions never fed
the CPU-capacity detector, so the host loop feeding a compute-bound GPU
ran at the lowest CPU performance level.

Check what ensure_compute establishes under the one device-mutex section
that publishes, and run the admission passes only when that check
fails. Publish a dependency-free compute command from its submit ioctl
when its queue is bound and nothing has to wait, as renders do. Keep a
separate backlog per engine so that a command waiting in one engine's
scheduler does not stop the other engine's inline publication. Report
compute dispatch envelopes to the detector as render passes are.
A render whose pair was out on loan slept 200 us before trying again.
The event worker lends each pair with completions out for an off-lock
observation after every completion event. At queue depths of two and
more, completions and submissions interleave, so a submission often
found its pair on loan for a few microseconds and then slept the full
200 us.

Count the device's change notifications. Every pair return already
notifies under the device mutex. Wait for the notification under that
mutex, unless one arrived since the attempt started, with the same
deadline as before.
Object::initialize already clears the complete descriptor before calling
its encoder. Fill that zeroed representation directly instead of clearing
the same 16 KiB again. Name the encoder write_zeroed to make its precondition
explicit.

The firmware bytes and validation remain unchanged. The matched module
passed the targeted compute/synchronization gate and checked GPU readbacks.
A publisher waiting for a physical compute channel reclaimed idle USC
backing even though freeing backing cannot create another QID. The normal
warm-pool limit could also evict a retired channel while another runnable
publisher was waiting to reuse it. Subsequent handoffs repeatedly allocated
and cleared fresh pool backing.

Distinguish channel pressure from backing allocation pressure. Track unbound
compute admission through publication, cancellation and error, and retain
backing for the existing compute channels while that demand remains. The
last publisher schedules normal idle reclamation. Allocation-pressure
reclamation, the idle warm-pool limit and expiry remain in force. Expose the
admission count in the capacity diagnostics.

On Neo, the matched 16-client probe drops pool allocations from 4888 to 53
and median submission time from 257 to 109 us. The Asahi KUnit suites pass
67/67; the targeted compute/synchronization gate passes 116 with 18 skips.
Full mustpass qualification remains pending.
Fence arrays require every member to signal. During syncobj timeline walks,
a pending predecessor already proves that the array is incomplete, yet the
lazy signaled callback polls every successor too. Return false immediately
at that predecessor, keeping callback accounting and the enabled-signaling
path unchanged.

Add checks for out-of-order completion, avoiding successor polls, and
callback installation after a partial lazy poll. Remove the obsolete
signal-on-any description from the constructor documentation.
Compute-only contexts that yield their physical channel still take the
device mutex in the inline submit path before falling back to admission.
Track an advisory binding hint to skip that attempt. Actual publication
retains the locked VM, epoch and binding checks, and scheduler admission
ordering is unchanged.

Mark contexts with validated rendering before any batch member is queued,
so mixed-engine queues retain the original inline path.

Validated with 69 Asahi KUnit cases, targeted compute/sync CTS (116 pass,
18 skip), mixed device churn and 16-client concurrent GPU readbacks.
Matched 16-process quick submission time improves from 117.27 to 89.55 us;
paired render follow-ups remain at control performance.
Only bypass an unbound compute-only inline attempt when multiple
publishers are waiting for admission. A lone admission retains the
original probe and its latency behavior. Actual publication still uses
the existing locked state checks.

Matched quick controls retain queue signaling latency (174.1 to 174.5 us)
while 16-process submission time improves from 117.3 to 90.7 us. All 20
performance rows complete successfully. Checked mixed/concurrent GPU
readbacks, 69 Asahi KUnit cases and targeted compute/sync CTS also pass.
Recognize eligible singleton compute producers through their exported aggregate
fences. Order queued consumers after actual producer publication using the
existing DRM scheduled fence, then validate the current physical ticket and
emit firmware waits. Preserve CPU waits for unsupported dependencies and host
outputs, and revalidate recovery epochs before acquiring consumer capacity.

Retain source binding loans through consumer retirement, propagate source errors
before dependent completion, and release failed submission pins when the last
runtime owner retires even if exported fence references survive. Keep bounded
member counters in byte atomics so submission state retains its original
allocation class.

Validated with 78 KUnit tests, the compute/synchronization CTS gate, chained
readback, process exits and mixed compute/render pressure. Fixed-core ordinary
compute matches the control while the queue-hop path improves. Existing
intermittent unrelated-client loss during forced recovery remains unresolved.
… queued

Compute retirement removes the producer from its queue's active list under
the device mutex, but signals the producer's fence only after the lock is
dropped. A consumer published in between found neither an active producer
nor a completed one, fell back to the scheduler, and every later command in
the chain then waited for its producer's publication through the scheduler.

Mark the singleton firmware source retired as its successful retirement is
queued (errors are already recorded there), after rechecking its own inputs.

sync.signal queue-to-queue (timeline, 2 queues): scheduler fallbacks for
chain consumers 57% -> 0.1%.

(cherry picked from commit a3be7f63500c76d117000ee1474713e27889aabe)
Early publication was limited to queues with fewer than two outstanding
commands, so every deeper queue paid a scheduler worker hop per command and
cross-queue chains advanced one scheduler round trip per link. The backend
already falls back to the scheduler behind any backlog and on every capacity
miss, which preserves submission order, so the depth limit only added
latency.

sync.signal queue-to-queue (timeline, 2 queues) 94.1 -> 56.2 us/hop together
with the preceding change; compute and render depth rows unchanged.

(cherry picked from commit 8508510641edcedb5ae1417f560cdff4da646bda)
Render descriptors already come from the retained object pool with a
firmware-cached mapping; compute descriptors were mapped uncached, so the
firmware read every compute descriptor from memory on each command.

Allocate the compute descriptor ring from the same pool (fully cached
prefix, retained until processor stop) and give the hardware an uncached
canonical low alias for the descriptor address it fetches.

compute.light d64 27.4 -> 24.3 us/submit, queue-to-queue hop 58.1 -> 54.8 us;
compute/sync CTS gate, cross-queue order/data 10k, dependent reads under
pressure and mixed churn pass.

(cherry picked from commit c85c050ae2ef77cbeebddb70c6602db477675090)
The compute channel budget was static: compute could only take a QID while
every one of the 58 possible execution contexts could still install a
render pair, which always left exactly ten compute channels. A client
without a channel then made every idle channel of every live client
shareable before binding one, so sixteen compute processes evicted each
other on nearly every submission (map/unmap and TLB invalidation churn,
ComputeReserve refused 52k times in one benchmark pass).

Installed QIDs never change engine before processor stop, so a compute
channel still permanently narrows render. Size the render reserve by the
logical queues that are actually open plus 24 queues of headroom, and
refuse a new logical queue only when its dedicated render pair would no
longer fit. With every context open this is the previous rule.

Rebinding now prefers a channel of a closed owner, then a vacant one to
re-arm, then a fresh QID, and only then shares the least recently used
idle channel of a single live client.

gpubench on Neo (us/submit, matched baseline): compute.light procs d16
92.0 -> 33.4; compute.light queue rows unchanged (d64 27.4 -> 26.5).
Stress gate passes.

(cherry picked from commit bf7ae5bf04b7d3f14b5ec94ef1f8c1434b4fe494)
A render whose attachment spans overlapped a live render of another logical
queue in the same VM waited in admission, on the host, until that render
retired. gpubench render.light queues renders every queue into one image, so
all queues ran one render at a time: run_job blocked for (N-1) renders and
the 2/4/8-queue rows stayed at one serialized step (174/201/217 us/submit).

Order such overlaps at publication instead. The device mutex already orders
publications; the later render's fragment kick now waits in firmware for the
newest published overlapping fragment kick of every other pair, so its tiling
kick stays concurrent and no host wait or scheduler stall is involved.
Published QIDs keep their kick rings, so these stamps only advance. An
admission bound on other pairs of the VM keeps the barrier count within its
fixed room. Cross-VM overlaps through shared objects keep the host wait, so
one VM's fault can never hold another VM's kick in firmware.

render.light queues d2/d4/d8: 173.8/201.3/216.9 -> 81.4/66.9/62.1 us/submit. Other
rows unchanged; the mild stress gate (CTS compute/sync, cross-queue order/data,
slow-read pressure, mixed churn) passes.

(cherry picked from commit 809d95cfdd685b6f94570cd313b1723c7f01f466)
Each compute command writes a queue configuration record, an optional
predecessor and completion-wait records, and an announcement; the firmware
read all of them through an uncached mapping. Render keeps its equivalent
configuration records cached and only its announcements uncached.

Move the configuration, predecessor and completion-wait records into a
retained pool object with a firmware-cached mapping at the same per-slot
offsets; announcements stay uncached.

compute.light d1 161.0 -> 152.2 us, d64 24.3 -> 23.1 us/submit, queue-to-queue
hop 54.8 -> 51.2 us; compute/sync CTS gate, cross-queue order/data 10k,
dependent reads under pressure and mixed churn pass.

(cherry picked from commit b3d5b9d6294bb46d63778264205da9d3b750651e)
…found

Every event pass with an unobserved compute command walked all 2048 records
of the compute completion ring under the device mutex to find the few it
needed; that walk was half of the event worker's CPU time.

Resume each scan after the last record the previous scan needed and stop as
soon as every requested identity has an observation. A scan that finds
nothing still wraps the whole ring once, as before.

scan_compute leaves the top of the 16-process profile; benchmark rows are
unchanged within noise (they are not bound by the scan). Compute/sync CTS
gate, cross-queue order/data 10k, dependent reads under pressure and mixed
churn pass.

(cherry picked from commit 67a49c9c40b31b96a7834c8c185dc20e0bac1fa2)
…mutex

Every compute publication took the device mutex, which the event worker
also holds for each service pass, and repeated device-wide work on every
submit (replay scan, render-prefix scan, binding checks).

Give each physical compute queue its own lock (a cell the registry owns;
device-mutex holders lock cells one at a time) and move the device-wide
state a publication needs into a per-session port that is reached under a
preparation lease: compute work-ring slot reservations, QoS accounting
under its own spinlock, the recovery generation, the full-effort flag and
the doorbell, whose RTKit borrows are serialized by a mailbox lock. The
steady-state path (bound, installed, no input sources, no render prefix)
publishes from the submit ioctl or run_job with only the queue's lock and
the port's small locks; everything else falls back to the device-mutex
path, which now reserves work-ring slots through the same port.

A command published while the event worker is scanning is not part of
that scan's completion snapshot and is left unobserved until the next one.

gpubench on Neo (us/submit, vs matched baseline): compute.light d1
166.3 -> 156.2, d64 27.4 -> 25.8, procs d16 33.4 -> 33.0 (firmware per
command time now bounds these rows). Stress gate passes.

(cherry picked from commit 8fc91b2117de925e362342a842b0d0befbba5312)
Render emits its queue configuration record only for an install, a context
or QoS update, or when the configuration differs from the last one the
firmware consumed in the current recovery epoch. Compute appended the record
to every command, so the firmware reapplied unchanged queue state each time.

Use the same rule for compute, caching the last consumed configuration with
its epoch and forgetting it when the item ring is reset.

compute.light d2 61.9 -> 51.5, d8 25.9 -> 24.3, d64 23.9 -> 22.8 us/submit;
queue-to-queue hop 51.1 -> 49.6 us. Compute/sync CTS gate, cross-queue
order/data 10k, dependent reads under pressure and mixed churn pass.

(cherry picked from commit 0e59b1f92effb58c87f705907959de45abe77623)
Waits for shared capacity sampled a condition and slept 1 ms before
checking again. Add availability domains: a waiter samples the domain's
generation before its failed check and registers a fence against that
sample (or retries when a release intervened); the release that provides
the resource bumps the generation and signals only that domain's
waiters. The fences can serve as scheduler prepare_job dependencies or be
waited on directly. The preparation gate can carry a domain that its
reopening, failure and removal signal.

Logical-queue creation is the first user: a creator refused for want of
a queue slot or an execution context held by a closing queue now waits
for the returned slot or the reclaimed context instead of sleeping in
1 ms steps, under the same overall deadline.

Tested on Neo as part of compute-wait2 (stress gate passes).

(cherry picked from commit 79cf4be7732d549b0f5d1f903bed7effdb80a4fb)
Compute admission and publication retried with sleeps: 1 ms for an
unpublished input source, a 20 us..1 ms backoff for a work state, up to
200 x 1 ms for recovery, 1 ms per ring backpressure retry, 200 x 1 ms for
a spared drain witness, and 1 ms per growth and channel capacity retry.

Use two availability domains for compute admission; the release that
provides the resource signals only the waiters of its own domain:

- channels: a queue becomes reusable, shareable or re-armable (checked
  on each transition in the retirement scan and pool service), a
  scheduler release is consumed, an owner closes, or an unpublished slot
  and QID are returned;
- progress: any compute retirement, finished replays, pool release
  observation, and the preparation gate reopening after recovery.

prepare_job now returns the channel, work-state or recovery fence a
compute packet still lacks, so run_job finds its capacity ready, and a
prepared packet claims its bound channel so the channel is not shared
away before run_job. Retries that remain inside publication block on the
same fences; the admission slice only bounds cancellation checks. Each
event pass also rechecks the level while waiters exist, which covers
capacity that appears through render (QIDs, work states, closed queues).

gpubench on Neo (us/submit, matched baseline): unchanged steady state
(procs d16 33.1, d64 25.9, d1 162); procs d2 93.2 -> 69.2. With the
channel budget forced back to ten channels (diagnostic build), sixteen
processes now share them at 51.9 us/submit instead of the old 92.0, with
no stall. Stress gate passes, also under the forced shortage.

(cherry picked from commit 25fb5800d97db7457f093ff41206047fbd9bddcc)
…record

A consumer of a firmware-visible compute source carried a completion-wait
record before its descriptor and also named the source in its kick entry.
The second dependency flagged the command as entry-signalled, so it also
appended a predecessor record, and the firmware resolved the same wait
twice.

Keep only the completion-wait record, which orders the consumer behind its
source by itself.

Queue-to-queue hop 49.6 -> 46.0 us. Cross-queue order and data checks (10k
iterations each), dependent reads under pressure, the compute/sync CTS gate
and mixed churn pass.

(cherry picked from commit 58ef65a530b707fb77878e9fdc68d577a27759fc)
Add tracepoints for each command's publication (doorbell rung, with its out
fence and the CPU counter), its retirement (with the firmware's GPU start and
end timestamps), physical queue binds and event worker pass boundaries. With
the counter and the GPU's system counter timestamps on the same events, a
trace places firmware dispatch, GPU execution, firmware completion and every
host stage of a command on one timeline.

The events are disabled by default and cost a static-key test each.

(cherry picked from commit 565dae906223948df97a252cc3274e8db6183363)
Compute rang its pipe and priority doorbell (pipe 2 in bits 1:0, class in
bits 3:2) only for the first publication of a priority class, then the
generic work doorbell for every command. Render rings its own pipe
doorbell for every publication, and so does the M5 compute path.

Ring the compute pipe doorbell for every publication instead of the
generic one.

gpubench on Neo (us/submit, on the integration branch): compute.light d2
60.3 -> 53.2, d64 22.8 -> 21.5, d1 163 -> 154..160, procs d16 unchanged
(27.2), hop unchanged (48.7). Stress gate passes.

(cherry picked from commit 398141cf95c44ac4d0073f3620a99d1235f31309)
Render keeps its queue records in a firmware-cached prefix; the compute
queue record, job-list head and item ring were mapped uncached. Allocate the
compute graph from the retained object pool with its 32 KiB queue record,
job-list head and item ring in a firmware-cached prefix, and move the
producer/consumer words to their own uncached page after it, as native
does.

compute.light d64 22.2 -> 19.7 us/submit, 16-process compute 27.2 -> 25.7,
queue-to-queue hop 47.5 -> 42.4 us. Compute/sync CTS gate, cross-queue
order/data 10k, dependent reads under pressure and mixed churn pass.

(cherry picked from commit 488708add18c39f709d6a9b53736d339428cf5d0)
Render admission and publication still slept or polled: a 20 us..1 ms
backoff when every work state of the context was taken, up to 200 x 1 ms
while recovery was observed, an unconditional 1 ms sleep for render
capacity (through the compute capacity wait), and a 1 ms condvar timeout
as the wakeup for a pair on loan, firmware room or a pending teardown.

Block on the event that ends each wait instead:
- a render availability domain, signalled by every admission-pool return
  (lease drop, owner close, quarantine change, exit-kill hold release,
  processor stop), every pair return from a loan, and on each event pass
  while a render publisher waits, since firmware ring room, consumed
  releases and teardown steps have no event of their own;
- a per-context work-state domain, signalled whenever a work state
  returns;
- the preparation gate: a publisher that observed recovery waits for its
  closure (close() now wakes the gate's waiters) and then blocks in
  enter() until recovery reopens it.
Render capacity first returns idle backing, then waits on the domain.
Waiters sample a domain only after a failed check and retry once before
blocking, so steady-state publication takes no extra lock. Deadlines only
bound the waits; 10 ms slices only bound cancellation checks.

Steady state is unchanged (render d1 135.7, d32 41.4, queues d8 62.6
us/submit); the mild stress gate passes.

(cherry picked from commit a3fc054bc52c2bc82350e6073366045a1c3c9489)
A logical render queue owns exactly one physical pair, whose active ring
holds RENDER_DEPTH (16) commands, but its scheduler had 32 credits. Jobs 17
to 32 entered run_job and parked there in the admission pool (or in the
1 ms busy wait) until a retirement freed a ring entry, holding the
scheduler worker. With the credit limit equal to the pair depth the
scheduler itself waits for the credit that the retirement returns, and
run_job never blocks on a full pair.

render.light d32 41.15 -> 41.26 us/submit (unchanged; the row is firmware
bound), all other rows unchanged, mild stress gate passes.

(cherry picked from commit 15de925f91cdb2a875e09fc1c6b64af79fa37ae2)
…is busy

A render pool whose last render retired sent its final FreeListRelease
right away: a control-ring record and a control doorbell for every render
of a one-deep queue. The firmware processes each such message serially
before it returns to the work rings, so with several queues active the
releases stretched every completion burst (one release per render in
render.light queues: about 44.6k per run).

When another pair still has published work, leave a registration whose
references drop to zero lingering instead; the owner's next render reuses
it like an overlapping render, with no release and no new generation. A
lingering registration ends, and its release is published, when its owner
closes or moves to another pool (each event pass checks) or after the
16 ms firmware idle hold, for which the event worker schedules a timed
pass. On an otherwise idle device the release is still sent at once:
reusing a registration that went idle costs the firmware more than the
release does (render d1 147 -> 184 us when every idle registration
lingered).

render.light queues d2/d4/d8: 80.8/67.3/62.6 -> 76.9/62.4/58.2 us/submit;
render d1 148, d32 41.3 (unchanged). Releases per queues-d8 run: 44574 -> 18
(measured with every idle registration lingering).
Mild stress gate passes.
A render publication into an owner's installed pair took the device mutex
six times from the scheduler (seven from the ioctl): to fetch the
admission pool (and, from the ioctl, again to try it), to check the pair's
identities, to find the pair already built (constructing a throwaway set
of build resources), to look for a sibling pair to warm, to lend the pair
out and to publish it.

The admission pool lives as long as the firmware session, so the logical
queue keeps a reference to it. The identity check also reports whether the
owner's pair is installed, idle and backed, in which case the first pass
skips graph construction; every retry still goes through it. An owner
has exactly one pair (kick.rs asserts it), so there is no sibling to warm.
Steady state now takes the device mutex three times from either path.

gpubench unchanged within noise (render d1 151.6, d32 42.8, queues d8
58.8 us/submit); the mild stress gate passes.
Every render publication held the device mutex, which the event worker
also needs for each pass, across the whole publication: dependency and
hazard collection, kick and descriptor commits, QoS accounting, both
work-ring records and producers, and the doorbell.

In the steady state (the owner's graph is installed and registered at its
priority, last ran this owner's scheduler, needs no growth, has nothing to
replay or settle, and the command has no compute prefix) the device mutex
now only lends the pair out, together with its free-list preparation and
a parameter-buffer reference, and takes it back. Preparation and
publication in between take the port: a render publication lock that every
render publication holds, with or without the device mutex (the tiling
and fragment work rings keep one host producer at a time, and attachment
overlaps keep their publication order), QoS accounting, the completion
stamps, the doorbell and the full-effort witness. The port gains views of
the render work rings and of the stamp page. The free-list lease leaves
preparation, or is rolled back with the parameter-buffer reference, when
the pair returns; while it is lent the event worker skips the pair, so no
retirement can touch its commands or references. A recovery that starts
during the publication fails it with EIO, as on the compute path.
Anything else, including a first publication, growth and owner changes,
still publishes under the device mutex.

During gpubench render rows 306k of 320k publications took the new path.
render.light d2/d8/d32 68.9/45.2/42.8 -> 65.9/43.5/41.1 us/submit, d1
150.2 (151.6), queues d8 58.7 (58.8); the rows stay firmware bound. Mild
stress gate passes.
Kernels built with CONFIG_WERROR fail on the G17 changes. trace.c defines
the eight tracepoint entry points that only Rust calls, so nothing in C
declares them (-Wmissing-prototypes). The Rust side also leaves code without
users: the compute host's generic doorbell, a recovery poll constant, the
old capacity-wait helper and an unused import, and, without G16, the CPU
timestamp mapping that only the G16 timestamp path uses.

Declare the entry points in trace_api.h, included by trace.c, remove the
unused items, and build the CPU timestamp mapping only with G16.

Reported-by: RyanTheTide
The scheduler fails a published job two seconds after it started unless
the job is a ray-tracing or acceleration-structure build. Under load that
timer measures how the GPU is shared rather than whether the job makes
progress. Beside four other clients, a 1.5 s compute dispatch took up to
5 s, and lower-priority work waited behind higher-priority work for
seconds, while the GPU kept retiring other commands at least every 1.2 s.
Those jobs failed with a lost device although nothing had hung.

Renew a published job's deadline while any command on the GPU retired in
the last five seconds, at most fourteen times (thirty seconds in all). A
GPU that retires nothing for five seconds has stopped, and its jobs fail
as before.
A client whose work failed closes its queues, and 250 ms later the driver
asks the firmware to kill the closed context's remaining commands. While
other contexts' commands are running, that kill makes the firmware restart
the GPU and fail everything in flight, and a few repeats crash it. On an
otherwise idle GPU the same kill is handled cleanly.

Publish the kill only while no other context has a render pair or compute
queue in flight, or once the GPU has retired nothing for five seconds, the
watchdog's liveness window. Until then the closed context's queues stay
quarantined and retained, and its commands retire like any others once the
GPU reaches them, as commands with ray traversal scratch already do.
Deferred kills are rechecked on every service pass.
…once

After a firmware restart, the firmware retires every render group it had
started on a pair without running it: the groups the recovery table lists,
and those whose tiler had run while their fragment stage was still pending.
Groups it had not started run as usual. The host republishes dropped
groups only when every retained group was dropped, so that none runs after
a live successor. Once a later group has run, the dropped ones never
qualify, and in-order retirement cannot pass the unexecuted front either.
The pair then waited for the render watchdog: about 2 s later before
deadline renewal, up to 30 s with it while other work keeps retiring. Its
owner then saw ETIMEDOUT and a poisoned VM, even when the client was
innocent and none of its work had started. In a stress run with forced
recoveries, 10 of 11 recoveries left such a pair.

Replay was also wrong for a dropped group that a compute command waited
on: the wait ends when firmware retires the group, so the consumer may
read output the group never wrote while the replay runs it afterwards. A
client in the same stress run read back the previous iteration's image
this way without any error.

When firmware has retired every group of the pair but a dropped group
cannot run again in order (a later group already ran, the dropped group
had started, or a compute command waited on it), fail the whole set at
once, after the same double observation as a replay. The groups that ran
fail too: they consumed output the dropped ones never wrote. The work is
innocent, so it fails with ENODATA, which its owner sees, without poisoning
the VM. Guilty and blamed sets are left to the timeout as before, and a
group that ran ahead of every dropped one is still retired in order.
A compute doorbell is withheld while two or more unretired commands of its
queue precede it, and any departure from ordinary retirement is meant to
ring it at once. A recovery that touches no command of the queue left it
withheld until those predecessors retired.

Once the firmware has resumed from a restart, ring every withheld doorbell,
one per priority class, before any replay is published.
Feature bit 17 is also used for render resources by the M5 userspace
line, whose features take bits 17 to 28. Advertise cumulative signals as
bit 29, which no line uses, so one Mesa can serve both kernels. A kernel
and Mesa that disagree on the bit fall back to ordinary per-point
signals, so either mix still works.
@DjDeveloperr

Copy link
Copy Markdown
Contributor Author

Thanks RyanTheTide. Status on each point:

  • trace.c prototypes. Fixed in 2d82861 ("build the G17 driver without warnings"). It adds g17/trace_api.h with all eight declarations, included by trace.c, and removes the Rust items the series left unused. Each commit of the series builds with KCFLAGS=-Werror; the remaining Rust warnings are toolchain fetch_update deprecations, almost all in base code.
  • Feature bit 17. Agreed: the M5 line uses bits 17–28. Cumulative signals move to bit 29, which no line uses, in lockstep in this PR and Mesa soc: apple: sep + dts: Touch ID on the 14" M1 Pro (J314s) #62. Old/new mixes fall back to per-point signals. The pair is built and goes through the same gate before I push it here.
  • Stress on this head. The regression report was against d0df244; the fix is in afbfbc0 (liveness watchdog + kill gating) and the PR body has the details. On the final kernel/Mesa pair, the same five-lane harness ran 20.5 min with 0 firmware restarts, crashes, host timeouts or kills, and 0 CTS crashes in 79 iterations. Before the fix: a collapse at 2.8 min and a firmware crash. Then the benchmark gate (full gpubench record, gate-eligible provenance) and targeted CTS (5368 cases, 0 fail) on that head.
  • Next on this PR. A stranded-work fix for genuine firmware recoveries (failing dropped render groups promptly instead of a 2–30 s watchdog, and never replaying behind a compute consumer). That pair gets the gate → sustained stress → CTS order again, then broader applicable CTS. Combination with drm/asahi: g17: keep innocent clients alive across another client's recovery #433 would need its own qualification, as you say.

…constructor

The test that grows compute QIDs with free identities still called
Registry::new() with the clusters and descriptor flags only, from before
the constructor took the deflake setting and the firmware variant, so a
KUnit build of the driver failed. Construct the registry as the other
tests do, for one cluster on the 26A428 firmware.
…e budget

The test that a firmware QID floor admits only the render pairs it leaves
reserved every compute channel before opening any logical queue. Compute
QIDs are now sized by the open logical queues, so on 25G83's floor that
first reservation was refused and the test panicked. Open the 42 logical
queues the floor allows without the up-front compute reservations, and
check as before that the next one is refused when it is created.
…ot the clock

The test that gate churn cannot renew a compute admission budget looped
until the budget ran out and then required the whole loop to have taken
less than 500 ms. The loop does not sleep, so that bound only measured
how long the test thread was descheduled, and a KUnit run under KVM on a
shared CI host exceeded it. Check the property instead: every admission
granted came from an attempt that started before the deadline, and the
first attempt after it is refused with EBUSY.
@DjDeveloperr

Copy link
Copy Markdown
Contributor Author

Broad applicable CTS on the final pair, as requested: this head's module (18b408b) with Mesa aurora-silicon/mesa#62 (309c3feea0e). Run on Neo, 7.3, 6 workers.

block cases Pass NotSupported Fail Crash
48 non-RT mustpass groups 1,650,061 681,296 968,761 0 0
ray_query 38,509 33,542 4,967 0 0
ray_tracing_pipeline 18,681 13,558 5,121 0 2

The 4 QualityWarnings are all in api.

The two crashes were build.cpu.level_geometries.aabbs_1_1048576_1 and build.cpuht_max.level_geometries.triangles_1_1048576_1. Both were killed by the system-wide OOM killer (global, not the run's memory limit) while 1M-geometry host builds ran side by side on a 7.5 GB machine with no swap. Their sibling cases passed in the same run. Run one at a time, both pass: 3.3 s with a 3.3 GB peak, and 5.5 s with a 3.5 GB peak.

integration/aurora moved since this was tested, and the PR now conflicts in asahi/Makefile and g17/mod.rs: adjacent additions, with the G15 objects and fwid.o on one side and the G17 C objects and trace module on the other. I merged integration/aurora into the branch, keeping both. On Neo the merge changes nothing in the G17 runtime path:

  • the new firmware-image check runs only for identified firmware;
  • the page-table change applies only with reserved tables (T6040);
  • the gem, mmu and G15 code is behind CONFIG_DRM_ASAHI_G15.

The merged module (de1be1b) was checked against the tested one, both with Mesa #62:

  • Regression gate: passes on both (cross-queue pair 229.6 vs 230.2 µs, order 131.2 on both, vkstress bad=0, no new faults or kernel errors).
  • Targeted CTS: 4687 Pass / 681 NotSupported / 0 Fail on both.
  • Light rows: pooled over eight boots in ABAB and BABA order, every row is within ±2%, except submit.empty d2 (−9%) and mem.image (+3.6%, inside its 184–206 µs range across boots for both modules).

@DjDeveloperr

Copy link
Copy Markdown
Contributor Author

A robustness issue in this driver under sustained ray-tracing load, reproduced on Neo with the merged module (integration 9cbdd07) and Mesa #62. This is relevant to the post-merge qualification on the distro bump PRs.

It took a 15-minute run of the multi-client stress harness plus looped ray_query.stress.geometry_shader.{triangles,aabbs}:

  • Unknown firmware event: the firmware reports a compute event with tag 7, which the driver doesn't decode yet. It came 26 times in that run, each followed by an ENODATA client failure.
  • Leaked queues: the queues behind those failures are quarantined, waiting for their retirement witness. Traversal queues are exempt from the close-time kill (exit_candidate()), because killing a traversal used to crash the firmware. Here the firmware never consumed their rings either, so 23 quarantined queues kept their lower-VA allocations for the rest of the boot.
  • Fallout: once the lower window was exhausted, new publications failed with EBUSY (426 in the run). RT stress cases lost their device in most runs, and other clients saw collateral failures. Fresh clients worked again once the load stopped, but most of the capacity stayed leaked.

A related wedge on an older build: a failed queue whose client was still open was never evicted, so later compute deferred behind it and timed out.

Fix in progress: first decode event 7, which is probably the firmware reporting an aborted command. If it identifies the command, the driver can retire it with an error and drain its queue. If it doesn't, the fallback is a bounded eviction of quarantined queues the firmware hasn't drained, open clients included. The pass bar is the same harness with no capacity leak and no EBUSY wave. I'll link the PR here.

@DjDeveloperr

Copy link
Copy Markdown
Contributor Author

Update on the robustness issue above: part of the fix is up as #496.

  • Event 7 decoded. It is the firmware's compute fault record: word 3 is the queue, and word 4 is the completion stamp the failed command would have stored but never does. drm/asahi: retire faulted G17 compute commands on the firmware's fault record #496 accepts that stamp as the failed command's retirement witness.
  • Effect. Under the same stress (10-minute runs), peak stuck queues drop from 23–27 to 10–13, lower-window refusals from 9486 to 166–2356, and EBUSY from 426 to 4–101. Gate, targeted CTS and the RT CTS groups are clean.
  • Not fixed yet. Queues with a command queued behind the failed one, whose context also holds a busy quarantined render pair, still leak until the firmware stops, so the window still runs out under sustained RT stress. On the unfixed driver that took about 5 minutes, and in one run gnome-shell then crashed on ENOMEM at about 7 minutes. The remaining case needs a render-side kill of the failed context's busy pairs, which comes as a follow-up.
  • Separate, pre-existing. When a ray query GS command fails beside other clients, the firmware also fails their concurrently running compute (event 7 on their own queues), and those clients see a lost device.

For the Neo sustained-stress part of the post-merge qualification: a long multi-client run that mixes RT stress with other clients can hit this, on the merged driver and with #496.

RyanTheTide added a commit that referenced this pull request Oct 26, 2026
List every #423 commit separately and enumerate the related integration
and aurora-wip history, including original/rebased aliases and Dj's
contributions. Record current reachability, firmware/data gates,
superseded policies, source evidence and a J616s measurement for each.

Separate public measured Neo results from stack results and estimates.
Document the applicable CLPC GPU-demand and PMP-voter ports and the
protocol-specific paths that cannot be enabled by copying Neo data.
Keep unmerged measured follow-ups outside the pinned-head claims.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
erikwb pushed a commit to erikwb/linux that referenced this pull request Oct 28, 2026
Trace calibration, firmware admission, queueing, power, recovery and
memory paths against the shared Neo backend. Separate changes already
present through aurora-silicon#463 and aurora-silicon#423 from firmware ABI restrictions and missing
hardware measurements. Record the public-source boundary for M5 Pro.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
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