Repository navigation
drm/asahi: g17: per-queue watchdog allowance for compute commands - #1752
Conversation
|
This advertises Two options: advertise the feature only where the allowance is the effective limit (M5) for now, or keep it on all G17 and say in the UAPI comment that on G17P the allowance can be extended by the shared renewals. We can run the PR's timing cases on an M4 Pro (J616s) if that helps. |
|
Thanks, you're right. I kept the feature on every G17 and fixed it in 8fdaa2b ("drm/asahi: g17: count an allowance against the shared renewals"):
Covered by the timeout unit tests (they apply the allowance and the shared renewals in the watchdog's order, with and without a publication wait); KUnit 603/601/2. I could only run hardware on the M5 Max, where the shared renewals don't apply, so yes please on running the timing cases on the J616s: with |
DjDeveloperr
left a comment
There was a problem hiding this comment.
Tested on the M5 Pro (MacBook Pro 14", J714s, T6050, G17S): all five checks pass. Approving.
Setup
- Kernel: our M5 default (integration/aurora
eb6b24140dc0) with this PR's first two commits applied (4860c0626b8c,652679a8a91a).- One adjustment: the first commit's UAPI hunk has the USERPTR define as trailing context, and that base lacks it.
- Only
aurora.kodiffers from the default, and it imports the same symbols as the default's module. Default Image, DT and loader; one boot.
- Userspace: aurora-silicon/mesa#259 (
4ba57b48,hk_compute_timeout_ms). An LD_PRELOAD shim logged eachqueue_create(size sent, request, value returned) and created extra probe queues.- Compute jobs are a dependent ALU loop of calibrated length: one workgroup, or 2048 workgroups of 64 (the GPU saturates at about 256).
- The render client is an offscreen 1080p draw loop.
- Third commit:
8fdaa2b26cbearrived during the run and wasn't rebuilt.- It only feeds allowance polls into
shared_renewals.renew_shared()is the only reader, andrenews_shared()is false on M5. - The runs below agree: with 30000 a 36 s job fails at 30.2 s, not about 58 s. With no request a 5 s job fails at 2.0 s while the compositor's work keeps retiring.
- So on the M5 these results hold for the current head.
- It only feeds allowance polls into
1. The default is unchanged. Same tests on the unmodified kernel and with the PR, no allowance anywhere:
| without the PR | with the PR | |
|---|---|---|
| 1 s compute, one workgroup, x3 | 982 / 979 / 979 ms | 983 / 979 / 979 ms |
| ~1.1 s GPU-filling compute, x3 | all complete | all complete |
| 1080p render loop, 5 s | 26,818 frames, p99 0.24 ms, max 5.1 ms | 26,668 frames, p99 0.24 ms, max 3.9 ms |
| gpubench render.light d16/d64, compute.light d1/d16 (us/submit) | 26.8 / 27.7 / 164.5 / 14.6 | 25.7 / 23.9 / 161.5 / 14.0 |
| ~5 s compute, no request | lost at 2030 ms | lost at 2003 ms |
- dmesg had no GPU watchdog or fault lines during the short jobs and the render.
- Without the option, Mesa sends the old struct size.
2. One ~5 s compute command
- No request: lost at 2003 ms.
hk_compute_timeout_ms=10000: completes in 4893 ms.- A ~14 s command with 10000: lost at 10092 ms.
- Each loss logs one
GPU work of a client failed (ETIMEDOUT …)line, naming only the test client.
3. The clamp. Bit 36 is reported. Values of compute_timeout_ms returned:
- 1, 500, 1999 and 2000 return 2000.
- 2001 through 30000 return the request.
- 30001, 60000, 600000 and 0xffffffff return 30000.
- pad = 1 returns EINVAL.
- The old 24- and 40-byte structs are accepted.
- A ~36 s job asking for 60000 is lost at 30230 ms.
On the unmodified kernel the same 48-byte requests come back unchanged (60000 returns 60000), because DRM copies the unknown tail back. Mesa#259 checks the feature bit, sends the old size there, and a 5 s job with the option set is lost at 2004 ms as before.
4. Sharing
- Two processes, one ~1.2 s GPU-filling job each, submitted together, two rounds each way:
- Without the allowance, the job published first is lost at its first poll (2022 ms, 2001 ms). The other logs
Compute watchdog … renewals=0 excused=0 decision=Defer … blocker_in_allowance=0and completes (2240 ms, 2580 ms). - With 10000 both complete (2271/2553 ms, 2266/2553 ms), and nothing is logged.
- Without the allowance, the job published first is lost at its first poll (2022 ms, 2001 ms). The other logs
- A ~25 s GPU-filling compute command with 30000. From 1 s in, three clients without an allowance ran beside it: the 1080p render loop, a GPU-filling compute job every 100 ms, and a one-workgroup compute job every 100 ms.
- The long command completes in 25.9 s.
- Render: 54,601 frames, longest 152 ms (17 frames over 100 ms).
- GPU-filling probe: 298 jobs, longest 190 ms. One-workgroup probe: 299 jobs, longest 17 ms.
- Nothing was lost, there were no watchdog lines, and the compositor was unaffected.
- On J714s the firmware runs the other clients' work alongside the long command. Nothing waited long enough to reach the 8-renewal deferral or the new
decision=Excuse, so this run doesn't cover that path.
5. abi.py, run on the M5 (gcc 16.1.1, rustc 1.98.1): test-uapi.py passes with 307 comparisons and 0 errors for intg6 plus this PR, for this PR's header, and for a header without the field. The three new _Static_asserts compile. The M5's own ld.lld couldn't load its libLLVM, an environment problem there and not this PR, so rustc linked with -fuse-ld=bfd.
CI: host-tests is green on 8fdaa2b2.
Nits (non-blocking)
- The UAPI comment says the driver "returns the allowance in effect here". A request of 0 comes back as 0, not 2000, because the field is only written for a nonzero request. Either say "for a nonzero request", or write the default back.
abi.py: the newdrm_asahi_queue_create:sizesubstitution sits inside theif 'struct drm_asahi_cmd_render_with_prior {' not in …block, as the existingclient_memory_limitone does.test-uapi.py --mesaagainst mesa#259's header, which still has that struct, therefore reportsdrm_asahi_queue_create:size40 vs 48. It also reports the olderdrm_asahi_params_global:size600 vs 616.- Both layouts are in fact 48 bytes with the field at offset 40.
- Fix: apply the two tail substitutions after that block, to any header that has the tail.
The scheduler's watchdog polls a G17 queue every two seconds and takes a compute command that a poll finds still running at the head of its queue to have hung: on the M5 it renews nothing, and the command fails about two seconds after it got there. A compute job that needs longer, a long prefill of an inference engine for example, has to be cut into small command buffers with few in flight, and still fails when other clients share the GPU: two processes each submitting a 1.1 s compute job that fills the GPU at the same moment ran side by side, one finished after 1.57 s and the other was killed 2.0 s after its submission, with about 0.3 s of its own work left. drm_asahi_queue_create gets compute_timeout_ms, advertised by DRM_ASAHI_FEATURE_COMPUTE_TIMEOUT (bit 36): a watchdog allowance for each compute command of the queue. The driver clamps it (at least the default of one poll, at most the compute_timeout_max_ms module parameter, 30000 by default) and returns the allowance in effect. At a poll, the watchdog now first renews a command that is within its allowance. Render commands keep the default. It is an allowance, not a deadline: - The watchdog still polls every two seconds, so a command may run up to one poll longer than its allowance. - The start of a command is known only approximately. The scheduler arms the watchdog when the job ahead completes, and again when a later job is pushed, so a job's first expiry comes at least one poll after it reached the head of the queue, later if pushes postponed it; the time is counted from one poll before that expiry. A job still in publication at an expiry (waiting for capacity) has not started: its time counts from the first expiry that finds it published. - Past the allowance, the renewals and deferrals that false_timeout() already grants still apply, in the same order as before: the execution renewals of ray and acceleration-structure work, the shared-work renewals (not on the M5) and the deferral of work behind another VM's earlier work. Under the default allowance the first expiry of a published job is not renewed by it, as before. The one change there: a job that was still in publication at an earlier expiry now gets one poll from the expiry that first finds it published, where before it could fail at that expiry however recently it had been published. There is no bound on the sum of the allowances: a queue kept full of long commands occupies the GPU for as long as it is fed. Nor are the other clients shielded from such a queue: they share the GPU with it as the firmware schedules them, and a command of another client can wait until a long command ends (the numbers are below). The struct grows at its end: a caller passing the old size gets the default, and a kernel without the feature ignores the field. Measured on the J716c (M5 Max MacBook Pro), with compute jobs of a single-thread loop of known length. Before this change: - one job of 2.13 s or 2.41 s: killed at 2.04-2.05 s (ETIMEDOUT, the client sees a lost device); one of 1.86 s: completes. - four jobs of 0.93 s submitted back to back on one queue, with and without timestamp queries: all complete, the last 3.71 s after its submission; two of 1.6 s: the second completes 3.23 s after its submission. Jobs queued behind their own queue do not lose time. - one 1.1 s job on each of two queues of one process: both complete (2.24 s for the second), both queues share one firmware queue. With it, and a Vulkan driver that passes a requested allowance on: - no request: a 2.13 s job is killed at 2.04 s, as before. - 30000 requested: jobs of 5 s and 20 s complete; a 40 s job is killed 30.7 s after its submission, as it is when 600000 is requested. - three 16 s jobs submitted back to back on one queue: all complete, 16.0, 32.0 and 48.0 s after the first submission. - two processes each submitting a 1.1 s job that fills the GPU at the same moment: both complete, after 1.93 s and 2.37 s. Sharing, measured with one client keeping a queue full of compute jobs that fill the GPU (640 threads), of 1.5 s under the default allowance and of 5 s and 20 s with 30000 requested, while a second client submits a short compute job every 100 ms (a single thread of 5 ms, or 5 ms of work filling the GPU) under the default allowance: - On the integration/aurora of 2026-11-09 (about 1000 jobs of each kind beside the long jobs): the second client's jobs took a median of 6 to 9 ms and at most 105 ms, whatever the length of the first client's jobs. The desktop compositor's render jobs, about 1 ms on an idle GPU, took up to 0.7 s beside jobs of any of the three lengths, and 4 to 5 of them completed per second beside the 5 s and 20 s jobs against 9 to 10 beside the 1.5 s jobs and 43 idle; six of its jobs waited between 1.4 s and more than 2 s beside the long jobs, none beside the 1.5 s jobs. - On the integration/aurora this change is based on (about 730 jobs of the kind that fills the GPU): most took 6 ms, but six waited until about the end of the first client's running job: 1.5 s beside a 1.5 s job, and 3.4, 4.5, 5.7, 8.3 and 18.4 s beside 20 s jobs (the 4.5, 8.3 and 18.4 s ones with the first client's queue at low priority, which did not help). The second client's watchdog deferred the ones past two seconds behind the other VM's work; the last one ran out of deferrals after 18 s and failed with ETIMEDOUT, 2.3 s before the first client's job ended. So on the current tree a queue with a long allowance can hold up another client's command for as long as its own command runs, and past 18 s that client's work fails. Under the default allowance the same wait is bounded by the two seconds a command may run. Tests: the timeout tests (g17-ray-query-timeout-tests.sh on the host, 23 passed, and KUnit) cover the requested allowances, the default, a 30 s allowance counted from the head of the queue and ended at the first poll past it, a first expiry delayed by later pushes, and a job that waited in publication. The asahi-m4 layout probe compares the frozen queue_create bindings as a prefix of the grown struct and asserts the new tail. Developed with Claude Code. Signed-off-by: Erik Bourget <erik@ebourget.net>
…llowance With a compute allowance, one client's command may run for up to compute_timeout_max_ms (30 s by default), and another client's command, render or compute, can wait on the GPU until it ends. The waiting job's watchdog defers it behind the other VM's older work at most eight polls (MAX_RENEWALS, 16 s) and then fails it: a compositor frame that waited behind a 20 s compute command of another client failed with ETIMEDOUT after about 18 s, and the compositor lost its device. A job's watchdog now records when its allowance ends in the packet, and the cross-VM heads carry whether that job is still inside it. A waiting job whose blocker is live and inside its allowance is renewed with a new decision, Excuse, which does not use its eight renewals. Once the blocker completes, or its allowance has passed, the existing rules apply again, with what the job has left of its eight renewals for the blocker's own failure and teardown. Unchanged: - A blocker that already failed excuses nothing, as before. - The oldest unfinished work is never excused: it has no blocker. - A blocker under the default allowance excuses nothing, so clients that do not ask for an allowance see the decisions they saw before. Render and compute watchdogs decide through the same attribution, and both see compute heads as blockers, so a render job waiting behind a long compute command is covered too. The excuse is bounded on the waiting job's side: at most compute_timeout_max_ms in polls (15 with the default 30 s), however many blockers take turns. This rule therefore renews a job at most 15 + 8 times by default, 46 s of renewals. A job's own allowance and its other deferrals are counted separately and come on top, and each renewal re-arms a timer rather than an absolute deadline, so the time until a waiting job is failed (spared) can be longer than that. The watchdog lines for cross-VM decisions now print the excused polls and whether the blocker was inside its allowance, and are written at the first excuse, the first counted deferral and the final decision. Tests: KUnit and the host timeout tests cover a live blocker inside its allowance past eight renewals (excused), the excuse used up (renewals, then spared), a blocker past its allowance and a failed one (the decisions as before), no blocker and the oldest work (own), the bound, blockers taking turns (the counters are the waiting job's and are not reset), and when an allowance ends. Developed with Claude Code. Signed-off-by: Erik Bourget <erik@ebourget.net>
On the G17 GPUs other than the M5, the watchdog renews a published job for up to fourteen polls while the GPU keeps completing other work (renew_shared), which bounds a job under the default allowance at thirty seconds on a shared GPU. These renewals were tried once a queue's allowance had passed, so the two added up: with the longest allowance (30 s by default) a command on a busy GPU could run for about 58 s, and a 40 s command could complete there instead of failing at about 30 s as it does on the M5, where the shared renewals do not apply. A poll that an allowance renews beyond what the default allowance renews now counts as one of the job's shared renewals. The two run from the same start, and a job is renewed for the longer of its allowance and the thirty seconds sharing gives any job, not for their sum. The one poll every allowance renews after a wait in publication is not counted, so asking never leaves a job fewer polls than the default. For an ordinary compute command that never finishes, polled every two seconds: - With an allowance of 30 s it is failed one poll after 30 s on a shared GPU or alone, on every G17, instead of after about 58 s on a busy G17 other than the M5. - With a shorter allowance on a shared GPU it is failed at thirty seconds, as a command without an allowance is. - Under the default allowance nothing changes. The renewals and deferrals that do not depend on sharing still come on top, on every G17 as before: the extra polls of ray and acceleration- structure work, and the deferrals of work waiting behind other work. A poll that runs late also still moves the estimated start, as the comment of compute_timeout_ms says; that comment now also says that the shared renewals run alongside an allowance. Tests: the timeout unit tests (KUnit and host) apply the allowance and the shared renewals in the watchdog's order to a job that never finishes: under the default allowance it fails at 2 s alone and 30 s on a shared GPU; at 30 s alone and shared with 30000; at 6 s alone and 30 s shared with 6000; at 30 s shared with 2001 and 20000; at 40 s with 40000; and after a wait in publication every allowance from the default to 30000 fails at the same poll on a shared GPU. This was not run on a G17 other than the M5 Max, where the shared renewals do not apply. Developed with Claude Code. Signed-off-by: Erik Bourget <erik@ebourget.net>
8fdaa2b to
772bb5b
Compare
|
@DjDeveloperr thanks for the thorough run on the J714s. Both nits are fixed and folded into the first commit (the branch is still three commits, the other two are unchanged; head is now 772bb5b):
checkpatch 0/0, host tests 288/293 (the five failures are the known apple-dcp and g15 toolchain ones). Fun fact: this fix was written by Qwen3.8-Flash-Next running locally on the J716c itself (M5 Max, Linux, on this PR stack and the open Mesa PRs), driving a coding agent through its OpenAI-compatible server. Two rounds, 43 requests, 14,133 tokens generated at 102.4 tok/s decode (104.4 tok/s on the first round), prefill about 1,320 tok/s, with contexts up to 39K tokens. |
RyanTheTide
left a comment
There was a problem hiding this comment.
The UAPI matches mesa#259 (offset 40, pad at 44, bit 36) and the default path is unchanged apart from the one extra poll after a wait in publication. The third commit only acts on G17 parts where the shared renewals run, and that is the M4 Pro and A18 Pro, so we will run the timing cases there before merging: 20 s and 40 s jobs with hk_compute_timeout_ms=30000 under a busy compositor, plus a default-path desktop and compute CTS run.
|
First results, with the three commits applied to integration e661192 in a one-shot test kernel on the M2 (G14G): desktop boot, glmark2-es2 9767 (production 9822, one short run), no watchdog lines and no GPU faults, 3 s2idle cycles clean. On G13/G14 this only covers the queue_create path with the old struct size. The G17 timing cases on the M4 Pro and A18 Pro are still to run. |
Brings in #1610 (GEM_RESIDENCY). It adds DRM_ASAHI_FEATURE_GEM_RESIDENCY (bit 35) at the place in include/uapi/drm/asahi_drm.h where this branch adds DRM_ASAHI_FEATURE_COMPUTE_TIMEOUT (bit 36). Both are kept, in bit order. No other change. Signed-off-by: Diljit Singh <hey@djdev.me>
|
@erikwb #1610 is merged, and it adds bit 35 at the same place in
Status
Non-blocking review notes (for a follow-up, not needed here; line numbers at
mesa#259 conflicts with aurora-wip in two files:
Unless you rebase first, I'll push that resolution to |
The problem
The scheduler's watchdog polls a G17 queue every two seconds and takes a
compute command that a poll finds still running at the head of its queue to
have hung. On the M5 it renews nothing, so the command fails about two
seconds after it got there. A compute job that needs longer (a long prefill
of an inference engine, for example) has to be cut into small command
buffers, and it still fails when other clients share the GPU. On the J716c,
two processes that each submitted a 1.1 s compute job filling the GPU at the
same moment ran side by side: one finished after 1.57 s, the other was killed
2.0 s after its submission with about 0.3 s of its own work left.
Letting one queue's commands run longer has a cost for the other clients: a
command of another client, render or compute, can wait on the GPU until a
long command ends. The waiting job's watchdog defers it behind the other
VM's older work at most eight polls (16 s) and then fails it, and that
client loses its device although it did nothing wrong.
The change
First commit:
struct drm_asahi_queue_creategetscompute_timeout_msatits end, advertised by
DRM_ASAHI_FEATURE_COMPUTE_TIMEOUT(bit 36) on G17:a watchdog allowance for each compute command of the queue.
poll, at most the new
compute_timeout_max_msmodule parameter, 30000 bydefault) and returns the allowance in effect; a request of 0 keeps the
default and is returned unchanged.
allowance.
padmust be zero. A caller that passes the old struct size gets thedefault. Render commands and queues that do not ask keep the default. One
case changes under the default: a job that was still in publication
(waiting for capacity) at one poll now gets one full poll from the first
poll that finds it published.
It is an allowance, not a deadline:
poll longer than its allowance.
from one poll before the first poll that finds the command at the head of
the queue. That is no earlier than the command got there and may be later:
commands submitted to the queue in the meantime postpone that poll.
still apply, in the same order as before: the extra polls of ray and
acceleration-structure work, the renewals while the GPU retires other work
(not on the M5), and the deferral of work behind another VM's earlier work.
Second commit: work waiting behind a command that runs inside its allowance
is not failed for that.
cross-VM attribution compares say whether a job is still inside it.
inside its allowance is renewed with a new decision,
Excuse, that doesnot use any more of its eight renewals. When the blocker completes, or
its allowance has passed, the existing rules apply again, with what the
job has left of its renewals for the blocker's own failure and teardown.
is never excused (it has no blocker), and a blocker under the default
allowance excuses nothing, so clients that do not ask for an allowance
see the decisions they saw before.
see compute heads as blockers, so render work waiting behind a long
compute command is covered as well.
compute_timeout_max_msin polls (15 with the default 30 s), however manyblockers take turns. This rule renews a job at most 15 + 8 times by
default, 46 s of renewals. A job's own allowance and its other deferrals
are counted separately and come on top, and each renewal re-arms a timer
rather than an absolute deadline, so the time until a waiting job is
failed can be longer than that.
Compute watchdog ...andRender watchdog ... behind earlier work of another VM) also printexcused=andblocker_in_allowance=, and are written at the first excuse, at the firstcounted deferral and at the final decision.
Third commit: on the G17 GPUs other than the M5 the watchdog also renews a
published job for up to fourteen polls (thirty seconds) while the GPU keeps
completing other work. Those renewals came after an allowance, so the two
added up: with 30 s requested, a command on a busy G17P could run for about
58 s. A poll that an allowance renews beyond what the default allowance
renews now counts as one of those renewals, so a job is renewed for the
longer of its allowance and the thirty seconds, not for their sum:
fails one poll after 30 s on a shared GPU or alone, on every G17.
without an allowance does; the one poll every allowance renews after a
wait in publication is not counted, so asking never leaves a job fewer
polls than the default.
of waiting work still come on top, on every G17 as before. The UAPI
comment now says the shared renewals run alongside an allowance.
There is still no bound on the sum of the allowances: a queue kept full of
long commands occupies the GPU for as long as it is fed, and the other
clients share the GPU with it as the firmware schedules them. The UAPI
comment says what a waiting client can expect.
Testing
Hardware: M5 Max MacBook Pro (J716c).
Build:
integration/aurora6970a51 plus this branch, with the J716cconfiguration. No warning in the files the branch touches.
KUnit, arm64 with 16K pages in QEMU:
./tools/testing/kunit/kunit.py run --arch=arm64 --kunitconfig=drivers/soc/apple/sep.kunitconfig:Ran 603 tests: passed: 601, skipped: 2.asahi_g17_execution_allowancehas 31 tests, twelve of them new: the allowance (four), and for the waiting
client a live blocker inside its allowance past eight renewals (excused),
the excuse used up (renewals, then spared), a blocker past its allowance
and a failed blocker (the decisions as before), no blocker and the oldest
work (own), the bound, blockers taking turns (the counters are the
waiting job's and are not reset), and when an allowance ends (seven), and
the allowance and the shared renewals overlapping (one). The existing
test of the watchdog line also checks when an excuse is reported.
Host tests, as CI runs them:
python3 -I tools/asahi/run-host-tests.py:288/293 passed. The five that fail (apple-dcp,g15-pb-growth-testsand
g15-stats-drain-tests, each of the last two also with--mutations)fail the same way on
integration/auroraon the build host, which lacksthe Rust 1.93.1 toolchain the g15 tests ask for.
g17-ray-query-timeout-testsruns the 31 timeout unit tests,queue-priority-testscompiles the queue admission gate as before, andthe
asahi-m4layout probe compares the frozenqueue_createbindings asa prefix of the grown struct and asserts the new tail.
scripts/checkpatch.pl: 0 errors, 0 warnings for each commit.On the J716c (first commit, on the
integration/auroraof 2026-11-09 withthe then-open J716c pull requests), with Mesa
aurora-wipplus ahoneykrisp change that passes a requested allowance to the new field
(driconf
hk_compute_timeout_ms), and a Vulkan test program whose computejobs are a single-thread loop of known length:
ETIMEDOUT), as before the changeThe kernel log of the run has the three expected
ETIMEDOUTlines, eachnaming the test process, and nothing for any other client.
Sharing, first commit only, on
integration/auroraea10c4d: oneclient keeps a queue full of compute jobs that fill the GPU (640
threads), of 1.5 s under the default allowance and of 5 s and 20 s with
30000 requested; a second client under the default allowance submits a
short compute job filling the GPU every 100 ms. Most of the second
client's jobs took about 6 ms (p90 at most 6.5 ms); now and then one
waited until the first client's running job ended (1.46 s beside a 1.5 s
job; 3.4 to 8.3 s beside 20 s jobs), and one that waited behind a 20 s
job was failed after 18.4 s with
GPU work of a client failed (ETIMEDOUT, ...)following eight deferrals (decision=Spare). Thatfailure is what the second commit removes.
With both commits, on the J716c (the base above plus this branch):
client's 28.9 s compute command (allowance 30 s) waited 26.9 s and
completed. One
Compute watchdog ... excused=0 decision=Excuse ... blocker_in_allowance=1line; nodecision=Spare, no failed client.20.7 s and a 28.9 s command: 116 and 224 jobs, none failed, longest
waits 15.0 s and 15.7 s, one or two
decision=Excuselines per run.Render watchdog ... decision=Excuselines and oneCompute watchdog ... decision=Excuseline for the compositor, which kept its device.2.02 s; with an allowance of 30 s, jobs of 5 s and 20 s complete, a 40 s
job is failed at 30.7 s, a request of 600 s behaves as 30 s (30.7 s),
and three queued 16 s jobs complete at 16, 32 and 48 s.
Depends on
Nothing in the kernel. The Vulkan side is a separate pull request against
aurora-silicon/mesa.
Not covered
the field there is refused with
EINVAL.waiting work.
shared renewals do not apply); it is covered by the unit tests, which
apply the allowance and the shared renewals in the watchdog's order,
with and without a wait in publication.
🤖 Generated with Claude Code